프레임워크/Spring

[Spring] 스프링 입문강의[6] - 회원 관리 예제_백엔드 개발 / 스프링 빈과 의존관계 설정

:) :) 2023. 5. 29. 10:05
 

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

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

<김영한의 스프링 입문강의 참조>

 

 

0. 순서

  • 비즈니스 요구사항 정리
  • 회원 도메인과 repository 만들기
  • 회원 repository testcase 작성
  • 회원 서비스 개발
  • 회원 서비스 테스트
  • 스프링 빈
  • 의존관계

 

1. 비즈니스 요구사항 정리

 

  • 데이터 : 회원 ID, name
  • 기능 : 회원 등록, 조회
  • DB는 선정되지 않음

* 회원 Repository는 interface로만 구현한다. DB를 선정하지 않아서다.

* DB를 미선정했으므로 간단하게 Memory MemberRepository를 사용해 데이터를 저장할 것이다(구현체 기반).

 

 

2. 회원 도메인과 repository 만들기

 hellospring 폴더 속 domain, repository 패키지 폴더를 만든다.

domain 내부에 Member(회원) 클래스 구현,

repository 패키지 폴더 내부에 MemberRepository 인터페이스와 MemoryMemberRepository 클래스를 구현한다.

 

다음은 MemoryMemberRepository 클래스이다.

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<>();
    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 Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

Null value도 return 받을 수 있게 Optional.ofNullable키워드를 사용하였다.

 

이렇게 구현한 것들이 제대로 동작하는지는 테스팅을 통해 알 수 있다.

 

3. 회원 Repository TestCase 작성

간단하게 테스트하는 방법이 존재하긴 한다.

  • Java main method 에서 실행
  • 웹 어플리케이션의 Controller에서 실행

그러나 위 방법들은 낮은 가용성, 낮은 효율의 테스팅이어서 보통 JAVA에서는 JUnit이라는 프레임워크를 통해 테스트를 진행한다.

 

test폴더에 repository 패키지를 하나 만들고 테스트 하고 싶은 메소드들을 작성하면 된다.

모든 함수에 @Test 라는 annotation을 달아준다.

 

 모든 테스트는 각 테스트가 일어나는 순서에 관계없이 정상 동작되어야 통과한다.

테스트가 순서에 의존하지 않게 하려고 모든 테스트가 끝날 때 데이터를 정리(삭제)해준다.

테스트할 구현 코드에

public void clearStore(){ store.clear();}

를 써주고

Aftereach 키워드를 사용하여 모든 테스트 메소드가 끝날 때 data를 clear해주는 과정을 넣으면 된다.

중요한 포인트이다.

 

 

 

*TDD - 테스트 주도 개발

테스트 과정부터 구현한 뒤 실제 작동 코드를 구현하는 개발 방법이다.

 

 

4. 회원 서비스 개발

  서비스를 개발해보자.

 중복된 이름은 받지 않는 회원가입 서비스를 만들어보자.

 

아래에 모든 정보(Member)가 저장이 된다.

    private final MemberRepository memberRepository = new MemoryMemberRepository();

 

    public Long join(Member member){
        // 동일한 이름을 가진 회원은 가입 못하는게 비즈니스 로직
        // 아래는 중복회원 검증 메소드
        validateDuplicateMember(member);

        memberRepository.save(member);
        return member.getId(); // id 반환하게 만듦
    }

    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);
    }

ifPresent 키워드는 앞의 객체가 NULL값을 가지지 않을 때 다음 구문을 실행시키는 키워드이다.

 

validateDuplicateMember는 원래 join 메소드 내에 있던 로직이었는데, 이와 같이 일련의 로직을 가지는 부분은 Ctrl+Alt+M 을 사용하여 메소드 추출 리팩터링을 해 추출하여 사용하는게 좋다.

잘 개발한건지 테스트를 해보자.

 

5. 회원 서비스 테스트

 인텔리제이 IDE에서 Ctrl + Shift + T 단축키를 사용하면 테스트 틀을 알아서 생성해준다.

좌측에 서비스 폴더와 테스트 클래스까지 아름답고 완벽하게 만들어졌다.

 

테스트 메소드의 이름은 과감하게 한글로 작성해도 된다.

join() 을 회원가입() 으로 적을 수 있다(직관적이라 좋아서).

 

테스트 코드를 작성할 땐

//given

//when

//then

 

패턴을 사용하도록 하자(일반적으로 좋음).

 

  • given
    • 어떤 것이 주어졌을 때
  • when
    • 아래를 실행하면
  • then
    • 다음 결과가 나와야 한다.

 

 

Shift + F10을 누르면 이전 run과 똑같은 환경으로 다시 run한다.

 

 

 

 

5-1. 의존성 주입

 다음은 테스트 클래스의 구문 중 하나인데,

MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();

new 키워드로 각각 생성했기 때문에 서로 독립적이라 테스팅의 의미와는 맞지 않다.

 

따라서 테스트 할 original class에 

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

구조체를 삽입해준다.

 

그러고 테스트 class 에서

MemberService memberService;
MemoryMemberRepository memberRepository;

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

라고 쓰면 서로 종속된 관계를 가지도록 만들 수 있다.

이를 Dependency Injection, DI라고 한다(의존성 주입).

 

 

6. 컴포넌트 스캔과 자동 의존관계 설정

 MemberController가 MemberService에 의존하도록 관계를 설정해야 한다.아래를 잘 따라가 보자.

 

controller 폴더에 만든 HelloController, 지금 만들 MemberController 클래스파일 모두 내부 구현 중생성자 위에 @Controller 라는 키워드를 붙인 부분을 볼 수 있다.

이는 annotation이 붙은 객체를 스프링 실행 시 스프링 컨테이너에 담아 계속 저장하도록 한다.

이를 스프링 컨테이너에서 스프링 빈이 관리된다고 한다.

 

Service 파일과 Repository 모두 스프링 컨테이너에서 관리하기 위해 각각 역할에 해당하는 annotation,

@Service와 @Repository 키워드를 붙여줘야 한다.

 

Controller 내부에 MemberController라는 생성자들을 만들 때, Controller와 Service를 연결하는 과정에서도 주석을 붙여줘야 하는데, 이는 @Autowired 이다. 스프링에서 컨트롤러와 서비스를 자동으로 연결해주는 annotation이니까 Auto~ 구나 싶으면 된다.

연결이 되면서 Controller가 Service에 의존하게 되고, 이러한 의존관계 설정을 의존관계 주입(Dependency Injection; DI) 이라 한다.

MemberService 또한 MemberRepository를 사용하므로 이 둘을 이어주도록 @Autowired annotation을 사용해야 한다.

 

@Autowired 키워드가 없으면 생성자 내부 로직에 따라 새로운 Service 객체 혹은 Repository 객체를 생성하게 되는데, 해당 주석으로 의존관계 주입시 각 생성자는 새로운 필요객체 생성없이 스프링 컨테이너에 들어있는 필요 객체들을 가져다가 사용하게 된다.

 

 위 방식의 DI는 컴포넌트 스캔이라 부른다.

그 이유는 각 annotation들이 component 객체의 specialization(구체화) 이기 때문이다.

이러한 과정 이후 스프링이 자동으로 의존관계를 등록한다.

 

* 컴포넌트 스캔의 대상으로는 HelloSpringApplication이 존재하는 메인 디렉토리, 즉 hello.hellospring 패키지의 하위 디렉토리만 포함된다. 

 

* 스프링 컨테이너에 스프링 빈을 등록할 때, 기본적으로 싱글톤 패턴(객체의 인스턴스를 오직 1개만 생성, 유지)을 적용한다. 따라서 같은 스프링 빈이면 같은 인스턴스라고 생각할 수 있다.

 추가 설정을 통해 싱글톤 패턴을 채택하지 않을 수도 있지만, 일반적으로 싱글톤 패턴을 사용한다.

 

 

결과적으로, 스프링 개발은

 Controller에서 외부 요청을 받고, Service에서 비즈니스 로직을 만들고, Repository에서 데이터를 저장 하는게 정형화된 일반적 패턴이다.

 

 

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

 컴포넌트 스캔 방법은 구현 클래스를 변경해야하는 상황에서 유용하지 않다.

이런 상황에선 configuration(구성)파일을 만들어 직접 스프링 빈에 구현 클래스를 등록하는 방법이 더 좋다.

 

 HelloSpringApplication이 존재하는 디렉토리에 SpringConfig라는 클래스를 생성해준다.

아래는 구현 코드이다.

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;

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

컴포넌트 스캔을 통해 스프링 빈을 컨테이너에 등록한 것과 동일한 결과를 가져온다.

 

이는 추후 구현 클래스의 변경 -Repository를 DataBase기반 Repository로 바꿀 때- 와 같은 상황에서 유용하게 쓰인다. memberRepository의 return 값만 변경해주면 되기 때문이다.

 

8. DI 방법

의존성 주입 방법에는

  • 필드 주입
  • setter 주입
  • 생성자 주입

이렇게 3가지가 있다.

field 주입은 구동 중간에 코드를 변경할 방법이 없어 별로 안좋고,

setter 주입은 중요한 객체(private)들을 아무나 막 건들 수 있게되는 위험이 있어(캡슐화가 안됨) 잘 안쓰인다.

의존관계가 서비스 실행중에 동적으로 변하는 경우는 거의(아예) 없으므로 생성자 주입을 권장한다.

 

생성자 주입은 아래처럼 memberService() 생성자 동작시 memberRepository()와 의존관계를 만들어 주는 것을 의미한다.

@Bean
public MemberService memberService(){
    return new MemberService(memberRepository());
}

 

* Configuration으로 스프링 빈에 등록하려면 컴포넌트 스캔을 위해 썼던 @Component 주석을 제거해야 한다. 그러나 @Controller의 주석은 제거하면 안된다.

 

* @Autowired를 통한 DI는 각 클래스가 스프링 관리하에 있을때만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.