본문 바로가기
TIL

2023-10-27-TIL

by raphael3 2023. 10. 27.
반응형

서비스 만들기

리포지토리와 도메인을 만든 이전 노트에 이어서 오늘은 서비스 부분을 만든다.

서비스란? ”회원은 중복가입이 안 된다” 등의 비즈니스 로직을 말한다.

이번 노트에서는 다음의 비즈니스 로직들을 구현한다.


hellospring 아래에 service 패키지를 만들고 MemberService 클래스를 추가한다; src/main/java/hello/hellospring/service/MemberService.java


회원 서비스를 만들려면, 회원 정보를 담고 있는 도메인 객체인 리포지토리가 필요하다.

리포지토리란? DB와 가장 가까운 쪽에 위치한 객체로서, DB로의 접근, DB에 도메인 객체를 저장하고 관리하기 위한 계층이다.

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

		...
}

회원 가입 기능

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     회원 가입
     */
    public Long join(Member member) {
        Optional<Member> memberName = memberRepository.findByName(member.getName());
        memberName.**ifPresent**(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });

        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }
}

같은 이름을 가진 회원을 찾아내어 존재할 경우엔 예외로 처리하도록 한다.

이를 위해 Optional에 있는 ifPresent() 메서드를 이용한다. 파라미터로 받은 member의 이름을 memberRepository에서 찾고(findByName), 있을 경우(ifPresent), IllegalStateException 예외를 내도록 한다.

Optional이 보기 좋지 않으므로, 다음 코드와 같이 하는 것을 권장한다.

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원 가입
     */
    public Long join(Member member) {
        memberRepository.findByName(member.getName())
                **.ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });**

        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }
}

또한, findByName 이하로 로직이 길게 나오기 때문에 이는 메서드로 빼는 게 좋다. Cmd + Opt + M을 이용하면 쉽게 메서드로 변환 가능하다.

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원 가입
     */
    public Long join(Member member) {
        **validateDuplicateMember(member);**

        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }

    **private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }**
}

회원 조회 기능

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    ...

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

서비스 계층은 비즈니스 로직을 구현한다. 그래서 메서드들의 이름도 비즈니스적인 용어라는 것을 알 수 있다; join, findMembers, … 등


서비스 로직 테스트 하기

서비스 로직을 테스트하기 위해서, 테스트 코드를 작성한다. 테스트하고자 하는 클래스 이름에서 Cmd + Shift + T를 이용해 테스트 코드를 만들 수 있다.

또한, 테스트하고자 하는 메서드를 선택하는 것도 가능하다.

package hello.hellospring.service;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    @Test
    void join() {
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

테스트 코드 작성

테스트 코드에서는, 메서드의 이름을 과감하게 한글로 작성해도 된다. 또한, 테스트 코드는 실제 빌드 시에 코드에 포함되지 않기 때문에 이렇게 작성해도 무방하다.

package hello.hellospring.service;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    @Test
    void 회원가입() {
    }

    @Test
    void 전체회원조회() {
    }

    @Test
    void 특정회원조회() {
    }
}

테스트 코드 작성 시에는 given(:검증에 필요한 기반이 되는 데이터가 주어짐) / when(:검증의 대상이 위치함) / then(:검증이 실제로 이루어지는 부분임) 패턴을 추천한다.

package hello.hellospring.service;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    @Test
    void 회원가입() {
        // GIVEN
        
        // WHEN
        
        // THEN
        
    }

    @Test
    void 전체회원조회() {
        // GIVEN

        // WHEN

        // THEN

    }

    @Test
    void 특정회원조회() {
        // GIVEN

        // WHEN

        // THEN

    }
}

회원가입 테스트 코드 작성

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService = new MemberService();

    @Test
    void 회원가입() {
        // GIVEN
        Member member = new Member();
        member.setName("KwonTedd");

        // WHEN
        Long memberId = memberService.join(member);

        // THEN
        Member foundMember = memberService.findOne(memberId).get();
        assertThat(foundMember.getName()).isEqualTo(member.getName());
    }

		...
}

GIVEN 파트에서는 회원가입 기능을 테스트하기에 앞서 먼저 회원가입 기능에서 쓰일 데이터를 만들어줘야 한다. 따라서 member를 만들어주고, name을 설정한다.

WHEN 파트에서는 실제로 회원가입 기능을 실행한 후, 반환되는 결과를 저장한다.

THEN 파트에서는 WHEN 파트에서 반환받은 데이터를 이용해서 기대하는 결과가 나오는지를 검증한다. 이 코드에서는 memberService에서 반환받은 멤버의 id로 member를 찾은 후, GIVEN 파트에서 만든 member와 name이 같은지를 비교한다.


테스트는 정상 flow보다 예외 flow가 훨씬 중요하다. 즉, 회원가입 기능에서의 예외 flow인 ‘중복 회원이 저장되는 것으로 인해, 앞서 구현한 IllegalStateException 예외처리 메세지가 뜨는 상황을 볼 수 있는 것’이 필요하다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService = new MemberService();

    ...

    @Test
    void 회원가입_중복회원검증() {
        // GIVEN
        Member memberA = new Member();
        memberA.setName("KwonTedd");

        Member memberB = new Member();
        memberB.setName("KwonTedd");

        // WHEN
        memberService.join(memberA);
        try {
            memberService.join(memberB);
            System.out.println("예외처리가 수행되지 않음");
            fail();
        } catch (IllegalStateException e) {
            System.out.println("예외처리가 성공적으로 수행됨");
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
            // assertThat(EXPECTED).isEqualTo(ACTUAL)
        }

        // THEN
    }

		...
}

GIVEN에서 중복되는 이름을 가진 두 member를 생성하고, WHEN에서 이들을 실제 회원으로 등록한다.

등록할 땐 예외처리가 뜰 것이기 때문에, try-catch를 이용한다. try가 성공적으로 수행되면 구현한 예외처리가 정상작동하지 않은 것이고, 이 땐 fail()이라는 메서드를 호출하게 한다. 반면, catch로 넘어가면 중복 회원의 회원 가입을 막는 기능이 정상 작동한 것이고, 이 때에 대한 예외 처리 메시지가 구현해 놓은 메시지와 같은지를 assertThat으로 검증한다.


이를 쉽게 assertThrows()로 구현할 수 있다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService = new MemberService();

    ...

    @Test
    void 회원가입_중복회원검증() {
        // GIVEN
        Member memberA = new Member();
        memberA.setName("KwonTedd");

        Member memberB = new Member();
        memberB.setName("KwonTedd");

        // WHEN
        memberService.join(memberA);
        assertThrows(IllegalStateException.class, () -> {memberService.join(memberB);});
        // 테스트가 성공하면, 예외처리 메시지가 뜬 것이다.

        // THEN

    }

    ...
}

assertThrows(클래스, () -> {}): assertThrows는 파라미터로 먼저 실행했을 때 발생해야 하는 예외처리에 대한 클래스가 들어가고, 실행할 로직이 그 다음에 들어간다.

위 테스트의 결과가 성공으로 뜨면, 예외처리가 성공적으로 수행된 것으로 보면 된다.


실제로 발생한 메시지가 우리가 작성한 메시지와 동일한지까지 검증하기 위해, assertThat을 이용한다.

Cmd + Opt + V로 assertThrows의 반환값을 받을 수 있고, 이 반환값은 Exception이기 때문에 getMessage() 메서드를 이용해 실제 예외처리 메시지를 뽑아낼 수 있다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService = new MemberService();

    ...

    @Test
    void 회원가입_중복회원검증() {
        // GIVEN
        Member memberA = new Member();
        memberA.setName("KwonTedd");

        Member memberB = new Member();
        memberB.setName("KwonTedd");

        // WHEN
        memberService.join(memberA);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> {
            memberService.join(memberB);
        });

        // THEN
        assertThat(e.**getMessage()**).isEqualTo("이미 존재하는 회원입니다.");
    }

    ..
}

테스트 클래스를 run하여 모든 테스트 케이스를 돌리면 에러가 뜬다. 즉, 위 코드에 의하면 memberService.join(memberA);에서 테스트 실패가 뜬다.

이전과 마찬가지로, 테스트들 끼리 의존성이 있기 때문이다. 즉, Service에 대한 clear 기능이 필요하다. 하지만, clear 기능은 memberSerivce가 아닌 repository의 기능이다. 따라서, repository를 만들어준 다음 clear 기능을 넣는다. → 좀 이상함

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService = new MemberService();
    MemoryMemberRepository memberRepository = new MemoryMemberRepository();

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void 회원가입_정상작동여부검증() {
        // GIVEN
        Member member = new Member();
        member.setName("KwonTedd");

        // WHEN
        Long memberId = memberService.join(member);

        // THEN
        Member foundMember = memberService.findOne(memberId).get();
        assertThat(foundMember.getName()).isEqualTo(member.getName());
    }

    @Test
    void 회원가입_중복회원검증() {
        // GIVEN
        Member memberA = new Member();
        memberA.setName("KwonTedd");

        Member memberB = new Member();
        memberB.setName("KwonTedd");

        // WHEN
        memberService.join(memberA);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> {
            memberService.join(memberB);
        });
        // assertThrows(실행했을 때 발생해야 하는 예외처리: 클래스, 실행할 로직; () -> {})
        // 테스트가 성공하면, 예외처리 메시지가 뜬 것이다.

        // THEN
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

    @Test
    void 전체회원조회() {
        // GIVEN

        // WHEN

        // THEN

    }

    @Test
    void 특정회원조회() {
        // GIVEN

        // WHEN

        // THEN

    }
}

앞서 테스트를 위해 생성한 리포지토리는 MemberService에서 만든 리포지토리와 같은 리포지토리일까? 실은, MemoryMemberRepository를 보면 자료구조가 static으로 되어 있어서 인스턴스 레벨이 아닌 클래스 레벨에서 해당 자료구조가 쓰여서 작동은 하지만, 어쨌든 new를 이용하여 서로 다른 인스턴스로 만드는 중이고, static을 떼면 다른 객체를 생성하는 것으로서 정상 작동하지 않을 것이다.

즉, 지금 상황이 서로 다른 리포지토리를 만들어서 비교하는데 리포지토리 내부에서는 static으로서 동일한 DB를 쓰고 있는 것으로서, 조금 이상한 상황이다.


따라서, MemberService에서 직접 리포지토리를 생성하는 것이 아니라, 주입받는 방식으로 바꾼다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     */
    public Long join(Member member) {
        validateDuplicateMember(member);

        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

이와 같이 외부에서 생성된 객체를 주입받는 방식을 의존성 주입(DI)라 한다.

변경된 코드에 맞춰서 테스트 코드를 수정한다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    ...
}

BeforeEach 어노테이션을 이용하여 테스트 케이스가 실행되기 직전에 실행되는 콜백 메서드를 만든다. 이 beforeEach() 메서드에서는 리포지토리를 만든 다음, 해당 리포지토리를 클래스의 멤버 변수에 등록하고, 등록된 멤버변수 리포지토리를 서비스의 생성자로 주입시켜, 테스트 클래스의 멤버 변수에 등록한다.

이제, static을 제거해도 정상적으로 테스트가 이루어질 것이다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{
    private ~~static~~ Map<Long, Member> store = new HashMap<>();
    // 공유로 인한 동시성 문제를 해결하기 위해 concurrentHashMap을 사용할 것을 권장함

		...
}

근데, 위 방식은 매번 새로운 리포지토리와 서비스를 만드는 방식이기 때문에 실은 공통의 리포지토리를 이용하는 방식이 아닌 것으로 보인다. 그 이유는 afterEach 메서드를 제거해도 테스트가 성공적으로 수행되기 때문이다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemoryMemberRepository memberRepository = new MemoryMemberRepository();
    MemberService memberService;

    @BeforeEach
    public void beforeEach() {
        memberService = new MemberService(memberRepository);
    }
    // 테스트 케이스가 돌아가기 직전에 실행되는 메서드로서, 매번 memberService 객체를 만들어준다.

//    @AfterEach
//    public void afterEach() {
//        memberRepository.clearStore();
//    }

    ...
}

하지만 리포지토리를 공통으로 사용하게 변경하고 afterEach 메서드를 작동하게 하지 않게 함으로써 공통의 리포티토리를 clear없이 그대로 사용하게 했을 때, 중복 이름을 걸러내지 못하는 문제가 발생하는데 이유를 모르겠다.

validateDuplicateMember쪽이 문제인 것 같은데..

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     */
    public Long join(Member member) {
        validateDuplicateMember(member);

        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    System.out.println("EXCERPTION!");
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    ...
}

결국 별개의 리포지토리이기 때문에 발생하는 문제로 밖에 안보인다.


스프링 빈과 의존관계

앞서 리포지토리와 도메인, 그리고 서비스를 만든 것에 이어서 이번에는 컨트롤러를 만든다.

컨트롤러는 서비스와 긴밀한 커뮤니케이션을 통해 일한다. 즉, 멤버 컨트롤러가 멤버 서비스를 통해 회원 가입을 하고, 회원을 조회할 수 있어야 한다. → 즉, 컨트롤러가 서비스에 의존한다.


MemberController.java를 src/main/java/hello/hellospring/controller/ 이하에 생성한다.


package hello.hellospring.controller;

import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    ...
}

Controller 어노테이션에 의해, 스프링은 MemberController의 인스턴스를 생성하여 스프링 빈으로 스프링 컨테이너에 등록한다. 아래 그림에서 초록색 타원이 스프링 빈이다. 이렇게 등록된 스프링 빈은 단 하나의 객체로서, 싱글톤 패턴에 의해 생성된다.


package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    private MemberService memberService = new MemberService();
    
    ...
}

지금까지는 MemberService를 직접 생성했지만, 스프링 빈을 사용하기 위해서 지금부터는 필요한 것을 이렇게 직접 생성하는 방식 대신, 생성자를 통해 의존해야 할 대상을 주입받는 방식을 사용한다.

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    private MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

		...
}

생성자가 호출될 때, Autowired 어노테이션에 의해 스프링 컨테이너에 스프링 빈으로 등록되어 있는 MemberService 객체를 찾아서 연결된다.

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    private MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
		
		...
}

하지만, 현재 MemberService는 순수한 자바 객체일 뿐이기 때문에 스프링이 관리하지 않는다. 스프링에게 MemberService가 Service라는 것을 알게 하기 위해서는 Service 어노테이션을 붙여줘야 한다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class MemberService {
    ...
}

리포지토리도 마찬가지다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.*;

@Repository
public class MemoryMemberRepository implements MemberRepository{
    ...
}

MemberService 역시 MemberRepository에 의존하기 때문에, Autowired 어노테이션을 이용하여, 스프링 컨테이너로 관리되는 스프링 빈인 MemberRepository가 연결되게 만들어준다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
		
		...
}

따라서, 아래 그림과 같은 상황이 된다.

이제 HelloSpringApplication.java(메인 메서드)를 실행시키면 문제없이 되는 것을 확인할 수 있다.

여기까지가 의존성 주입을 통해 의존관계에 있는 객체들끼리의 연결이다.

@Autowired는 의존관계에 있는 빈들(Controller, Service, Repository)을 연결시켜 줌으로써, Controller가 Service를, Service가 Repository를 쓸 수 있게 해준다.


의존성 주입(DI)의 세 가지 방법

  1. 필드 주입
    • 한 번 세팅된 후, 다른 인스턴스를 주입해야 할 땐 해당 코드를 직접 변경하는 것 외에는 변경할 방법이 없다는 문제가 있다.
  2. setter 주입
    • public으로 계속 열려있다는 문제가 있다. → 아무 개발자나 호출할 수 있게 열려있는 상태다. ⇒ 변경에 취약해짐
  3. 생성자 주입*
    • 다른 인스턴스를 주입해야 할 때, 생성 시점에 원하는 인스턴스를 주입할 수 있고, 한 번 주입한 인스턴스를 도중에 바꾸지 못하도록 한다.
    • 실제로 의존관계가 동적으로 변하는 경우(서버에서 빌드되어 실행 중인 서비스 내에서 의존관계가 동적으로 변하는 경우)는 거의 없다. → 코드를 수정하여 다시 실행시킨다.

스프링 빈을 등록하는 두 가지 방법

두 가지 방법 모두 중요하다.

  1. 컴포넌트 스캔과 자동 의존관계 설정
    • @Component
      • @Component가 있으면 스프링 빈으로 자동 등록된다. (등록시엔 싱글톤으로 등록함)
      • Component 어노테이션이 Controller 어노테이션, Service 어노테이션, Repository 어노테이션의 내부 구현에 모두 들어있다. 즉, @Controller, @Service, @Repository는 @Component의 특수화된 케이스다.
      • 스프링은 @SpringBootApplication이 붙은 클래스가 속한 패키지 내의 모든 코드를 스캔하여 스프링 빈이 될 것들을 찾아낸다. 소속된 패키지 외의 영역에서는 컴포넌트 스캐닝을 하지 않는다. 이는 @ComponentScan을 가지고 있기 때문이다.
    • @Autowired
      • @Autowired에 의해 @Component에 의해 생성되고 등록된 빈들이 서로 연결되어 의존관계가 형성된다.
      • 스프링 빈으로 등록되지 않은 객체에서는 동작하지 않는다. 즉, @Component든, @Controller든, @Service든, @Repository든, 혹은 @Configuration이 붙은 Config 클래스에 @Bean으로 등록된 것이든, 어떤 방식으로든 스프링 빈으로 등록되어 있는 것에만 @Autowired가 동작한다.
    컴포넌트 스캔 방식은 정형화된 코드(컨트롤러, 서비스, 리포지토리 등)에 사용한다.
  2. 자바 코드로 직접 스프링 빈 등록정형화되지 않은 상황, 구현 클래스를 상황에 따라 변경해야 하는 경우 등엔 직접 스프링 빈으로 등록할 필요가 있다. → 코드 수정을 최소화할 수 있다.
  3. 설정 파일을 이용하여 스프링에게 무엇이 Bean인지를 알려주는 방식이다.
package hello.hellospring;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// Service와 Repository에 있던 @Service와 @Repository를 제거한 후
@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

기본적으로는 @Configuration과 @Bean을 이용하여 스프링 빈을 등록한다.

반응형

'TIL' 카테고리의 다른 글

2023-10-31-TIL  (1) 2023.11.01
2023-10-30-TIL  (2) 2023.10.31
2023-10-28-TIL  (1) 2023.10.31
2023-10-26-TIL  (0) 2023.10.27
2023-10-24-TIL  (0) 2023.10.24