Spring

Spring Bean - 공동으로 객체를 관리하는 방법, IOC, Bean, DI의 개념

hwlee9905 2023. 12. 27. 17:24

이제 실제 상황을 가정하여 예제로 Service, Repository를 구현해보고 전체적인 구조를 이해해보자

 

  • 도메인 객체 Member는 간단하게, name, id가 존재하고 getter,setter메소드만 만들어줄 것이다.
  • MemberRepository interface를 만들고 MemoryMemberRepository에서 관련 추상메소드를 구현할 것이다. 
  • MemberRepository interface 에서는 기본적으로 저장, id나 name을 매개변수로 받아 member를 반환하거나 전체 member를 리스트로 반환하는 조회 기능을 만들어줄 것이다.
public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

-> 여기서 Optional은 nullable값을 다루기위해 사용한다.

public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
	return Optional.ofNullable(store.get(id));
    }
    @Override
    public List<Member> findAll() {
    	return new ArrayList<>(store.values());
    }
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
            .filter(member -> member.getName().equals(name))
            .findAny();
    }
    public void clearStore() {
  	store.clear();
    }
}

 

위의 코드가 잘 작동하는지 확인하는 방법은 기능마다 console을 찍어 , 웹 애플리케이션의 컨트롤러를 통해서 비즈니스 로직이 잘 작동하는지 확인할 수 도 있지만..이는 매우 비효율적이고 초보적인 방법이다. 따라서 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다. 각 비즈니스 로직이 잘 작동하는지 테스트하는 테스트 케이스를 작성하는 것이 효율적일 것이다. 

@AfterEach
public void afterEach() {
    repository.clearStore();
}
@Test
public void save() {
    //given
    Member member = new Member();
    member.setName("spring");
    //when
    repository.save(member);
    //then
    Member result = repository.findById(member.getId()).get();
    assertThat(member).isEqualTo(result);
 }

JUnit은 다양한 어노테이션과 검증 메서드들을 제공하여 테스트 작성과 실행을 도와주는데,

  • @Test 어노테이션은 이 중에서 테스트 메서드를 표시하는 역할을 한다.
  • @AfterEach 어노테이션은 @Test 어노테이션이 선언된 메소드가 끝날때마다 실행되는 메소드이다. 
  • 테스트는 각각 독립적으로 실행되는 것이 좋은 테스트이며, 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다. 따라서 각 테스트가 끝날때마다 clearStore() 메소드를 사용하여 repository를 초기화 해줄것이다

또한 MVC패턴과 같이 테스트에도 쓰기 좋은 패턴이 있는데, given, when, then 패턴이다. save() 메소드를 살펴보면 when 에는 실제로 저장하는 부분 , given에는 상황이 주어지는 것을 볼 수 있고,then에는 when 결과를 검증하는 부분이 들어가 있는 것을 볼 수 있다.

 

then 부분에서 많이 사용되는 테스트 결과를 검증하기 위한 라이브러리중에서 대표적으로 JUnit과 함께 사용되는 AssertJ, Hamcrest, Truth 등이 있다.

assertThat은 값이나 객체의 동등성을 비교하는데 자주 사용되며, 테스트 코드를 보다 명확하고 읽기 쉽게 만들어준다.

위의 코드에서 assertThat(member).isEqualTo(result); 부분은 특정 조건을 검증하는 코드로, result 객체와 member 객체가 동등한지를 확인한다. 

 

따라서, when의 코드가 정상적으로 작동하지 않는다면 assertThat은 관련한 에러메시지를 반환할 것이다.

 

서비스 구현

 

실제로 서비스를 구현한 코드를 비즈니스 로직이라고 한다. 

 
public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
 /**
 * 회원가입
 */
 public Long join(Member member) {
     validateDuplicateMember(member); //중복 회원 검증
     memberRepository.save(member);
     return member.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);
    }
}

 

서비스 테스트 

 

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

    @AfterEach
    public void afterEach() {
    	memberRepository.clearStore();
    }
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
        () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

assertThrows 메서드는 특정 예외가 발생하는지를 검증하는 JUnit 5의 메서드이다. 이 메서드는 두 개의 인자를 받는데,

  • 첫 번째 인자: 예상한 예외 타입 (IllegalStateException.class).
  • 두 번째 인자: 특정 동작을 실행하는 람다 표현식

MemberService 클래스의 validateDuplicateMember() 메소드가 Message를 반환하므로 해당 Message와 일치하는지 assertThat을 통해 확인한다

 

테스트를 하던 도중 이상한점을 발견했다.  MemberService 클래스에서 memberRepository 객체를 생성하는 부분이 있는데, MemberServiceTest에서도 memberRepository 객체를 생성하는 부분이 있다.

 

이 둘은 엄연히 다른 객체이다. 물론, memberRepository 클래스의 store는 static 변수로 선언했기 때문에 다른 객체라도 같은 변수를 공유하지만 같은 memory를 공유하는데 다른 객체를 생성할 이유도 없거니와, 다른 객체를 사용하는 것은 후에 어떤 오류를 발생시킬지 모른다.

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

 

따라서 Memberservice 객체를 생성할때마다 MemberRepository 객체를 생성자 매개변수로 넣어줄 것이다. 이렇게 하면 Memberservice 객체를 외부 클래스에서 생성해도 같은 MemberRepository객체를 공유할 수 있을것이다. 이런식으로 외부에서 객체를 주입하는 방식을 Dependency Injection(의존성 주입) 이라고한다.

 

Dependency Injection, Bean, IOC의 관계

 

여기서부터 벽을 느끼는 사람이 많을 것이다.. 필자 또한 그랬다.. 하지만 스프링의 가장 중요한 개념중의 하나인 이부분을 잘 이해하고 넘어간다면 앞으로 Spring을 활용하는데에 있어서 큰 디딤돌이 되어줄 것이다.

 

DI의 개념에 대해서 설명하였는데, DI의 목적이 무엇이었는지 잘 되짚어봐야한다.. 왜 굳이 외부 클래스에서 객체를 주입하는가?

그 이유는 결국 공동으로 객체를 관리하기 위함이었다. 그래서 Spring에서 주요하게 사용되는 개념이 IOC이다

 

IOC(Inversion Of Control) : 제어의 역전 일단 딱보면 용어가 굉장히 어렵게 들리고 이해하기 어려운데 위의 예제를 다시 생각해보자

 

Context: 개발자는 B클래스와 C클래스에서 A객체를 생성하여 공동으로 사용할 의도를 가지고 있다. 개발자는 결국 B클래스와 C클래스에서 new 생성자를 이용하여 A객체를 생성하였는데, 각기 다른 클래스에서 생성된 A객체는 서로 다른 객체이므로 DI를 사용하여 같은 객체를 사용할 것이다.

 

  • 그렇다면, DI를 편리하게 자동적으로 해주자(IOC)
  • 공동으로 객체를 관리하는 상자(Spring Container)를 만들고
  • 상자에 객체(Bean)를 넣어놓고 꺼내쓰자 

원래는 new 연산자와 객체 생성자를 적절하게 활용하여 객체 하나하나를 개발자가 제어(control)하던 시점에서, framework가 이를 자동적으로 해주게 되었으니, 이를 제어의 역전이라고 하는 것이다.

 

  • Spring Container : 스프링에서 자바 객체들을 관리하는 공간
  • Spring Bean : 스프링에 의하여 생성되고 관리되는 자바 객체

 

스프링은 스프링 컨테이너에 스프링 빈을 등록할때 기본적으로 싱글톤으로 등록하는데, 특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴이다.  즉, 생성자를 통해서 여러 번 호출이 되더라도 인스턴스를 새로 생성하지 않고 최초 호출 시에 만들어두었던 인스턴스를 재활용하는 패턴이다.

Spring Bean 등록하기

 

이제 본격적으로 IOC를 활용하여 의존관계를 설정해줄 것이다. 일단, 컨트롤러 클래스를 만들자

@Controller
public class MemberController {
    private final MemberService memberService;
    @Autowired
    public MemberController(MemberService memberService) {
    	this.memberService = memberService;
    }
}

생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. 하지만 현재 memberService 객체는 스프링 컨테이너 등록이 되어있지 않으므로 오류가 발생할 것이다.

 

스프링 빈을 등록하는 방법은 2가지가 있다.

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기

컴포넌트 스캔 원리

  • @Component annotation이 있으면 스프링 빈으로 자동 등록된다
  • @Component 를 포함하는 다음 annotation도 스프링 빈으로 자동 등록된다.
    • @Controller
    • @Service
    • @Repository

 

자바 코드로 직접 스프링 빈 등록하기

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService() {
    	return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
    	return new MemoryMemberRepository();
    }
}

@Configuration annotation을 선언하고 해당 클래스에 @Bean annotation을 선언하고 반환값으로 스프링 컨테이너에 등록할 객체를 반환하면 된다.

 

출처

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

[지금 무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 - 인프런

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확인해주세

www.inflearn.com