Server

[Spring] Spring boot와 JPA 활용 - 애플리케이션 구현 준비, 회원 도메인 개발

nahyeon 2022. 6. 19. 23:29

애플리케이션 아키텍처는 단방향으로 가져가되, Controller가 Repository에 접근할 수 있는 유연한 설계를 가져간다.

 

계층형 구조를 사용한다. 

- controller, web: 웹 계층

- service: 비지니스 로직, 트랜잭션 처리

- repository: JPA를 직접 사용하는 계층, 엔티티 매니저 사용

- domain: 엔티티가 모여 있는 계층, 모든 계층에서 사용

 

회원 도메인 개발

 

회원 리포지토리 개발

MemberRepository.java

@Repository 는 @Component 로 컴포넌트 스캔의 대상이 되어서 스프링빈으로 자동 등록된다.

@PersistentContext는 JPA의 entitymanager로 스프링이 entitymanager로 주입받을 수 있다.

 

멤버를 가입시키는 save() 메서드는 영속성컨텍스트에 member 객체를 넣는다. 트랜잭션이 커밋되는 시점에 DB에 반영될 것이다.

JPA에서 em.persist()할 때, 영속성 컨텍스트로 member객체를 올리게 된다. key와 value가 생기게 되는데 key는 @GeneratedValue인 id 값이 된다.(key가 보통 PK 값) 

 

findOne()은 단건조회로, JPA의 find() 메서드를 사용한다. 

findAll()은 전체조회로, JPQL을 사용한다. SQL과 거의 비슷하지만 from의 대상이 entity가 된다. 

 

회원 서비스 개발

 

MemberService.java

@Service로 component scan 

@Transactional : 트랜잭션 안에서 데이터를 변경할 때 사용. Service 전체에 적용하게 되면 public 메소드들에는 전부 적용이 되게 된다. javax에서 제공하는 Transactional과 spring에서 제공하는 Transactional이 있는데 spring이 제공하는게 더 크므로 spring에서 제공하는 것으로 import해서 사용한다.  

 

@Transactional(readOnly = true) 로 옵션을 설정하면, JPA가 조회하는 곳에서는 성능을 좀 더 최적화한다. 읽기에는 가급적 readOnly = true를 넣어준다. 쓰기에는 Transactional을 냅두면 된다. (default가 false)

 

1. @Autowird

Spring이 spring bean에 들어있는 MemberRepository를 injection 해준다. (필드 injection)

그러나 필드 인젝션은 단점이 많다. 테스트할 때 access할 수 있는 방법이 없다. field이고 private 이므로 접근할 수 없다. 

 

2. setter injection

private MemberRepository memberRepository;

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

 

그래서 setter injection을 사용한다. test code 작성시 메서드를 통해 가짜 memberRepository를 주입할 수 있다.

 

치명적인 단점으로는 실제 애플리케이션이 돌아가는 시점(런타임)에 누군가가 이걸 바꿀 수가 있다. 실제로 애플리케이션 로딩 시점에 세팅 조립이 다 끝나버린다. 실제 애플리케이션이 잘 돌아가는데 바꿀 일이 없다. 그런 이유로 setter injection이 좋지 않다.

 

3. 생성자 injection

private final MemberRepository memberRepository;

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

 

요즘 권장하는 방법은 생성자 injection 이다. 최신 버전에서는 생성자가 1개인 경우엔 @Autowired를 쓰지 않아도 생성자 자동 주입이 된다. memberRepository는 바뀔 일이 없기 때문에 final로 선언해주는 것을 권장한다. final로 선언하면 생성자에 관한 컴파일 오류로 check 할 수 있기 때문이다. 

 

@AllArgsConstructor : 생성자 injection은 lombok의 @AllArgsConstructor를 MemberService에 적용하면 자동으로 만들어준다.

@RequiredArgsConstructor: @AllArgsConstructor보다 더 나은 선택지로 final이 있는 필드만 가지고 생성자를 만들어준다. (권장)

 

 

회원 기능 테스트

MemberServiceTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @PersistenceContext EntityManager em;

    @Test
//    @Rollback(value = false) 
    public void 회원가입() throws Exception {
        //given
        Member member = new Member();
        member.setName("kim");

        //when
        Long savedId = memberService.join(member);

        //then
        em.flush(); 
        Assertions.assertEquals(member, memberRepository.findOne(savedId));
    }

    @Test(expected = IllegalStateException.class)
    public void 중복_회원_검증() throws Exception {
        //given
        Member member1 = new Member();
        member1.setName("kim");

        Member member2 = new Member();
        member2.setName("kim");

        //when
        memberService.join(member1);
        memberService.join(member2); // 예외가 발생해야 한다.
        
//        try {
//            memberService.join(member2); // 예외가 발생해야 한다.
//        } catch(IllegalStateException e) {
//            return;
//        }

        //then
        Assertions.fail("예외가 발생해야 한다.");
    }
}

@RunWith(SpringRunner.class) : 완전히 Spring과 integration을 해서 테스트를 진행할 것

 

@SpringBootTest: Junit4를 실행할 때 Spring과 엮어서 실행할 거라는 뜻

 

@Transactional : JPA에서 같은 transaction 안에서 같은 entity(PK 값이 같다면)라면, 같은 영속성 컨텍스트에서 하나로 관리된다.

Transactional의 default는 @Rollback(value = true) 이다. 테스트 실행이 끝나면 영속성 컨텍스트에 insert 된 데이터가 모두 roll back 되게 된다. 그러나 insert문을 확인하고 싶다면 해당 테스트에 @Rollback(value = false)를 설정하고 확인한다.

 

em.flush();

해당 코드는 영속성 컨텍스트에 member 객체가 들어가 있는 상황인데. flush()를 함으로써 영속성 컨텍스트에 있는 변경사항을 db에 반영하게 된다. db에 쿼리가 강제로 나가는 것.

 

중복_회원_검증 테스트에서 memberService에 Join을 같은 이름의 객체로 두 번 join 함으로써 에러가 발생해야 한다. 

MemberService.java에서 validateDuplicateMember() 메서드로 중복예외처리 메서드를 만들어주었고 중복 시 IllegalStateException을 던지게 해주었다. 테스트에서 member2를 join할 때 예외가 발생해야 하고, 원래는 try-catch 문으로 작성할 수 있지만 spring이 제공해주는 @Test(expected = IllegalStateException.class) 를 사용할 수도 있다. 

 

해당 예외가 발생하지 않았다면, 테스트에 fail 한 것이므로 Assertions.fail() 로 확인한다.

 

 

resources는 기본적으로 main에 있는 resources가 우선권을 가지게되고,

test 폴더 경로에도 resources가 존재한다면 test/resources가 우선권을 가지게 된다.

고로 test 파일들을 돌리게되면 test 경로에 있는 application.yml이 적용되서 실행되게 된다.

 

build.gradle

라이브러리에 h2 database가 들어와있다면, 클라이언트 역할만 하는 것이 아니라 h2가 java로 구성되어 있기 때문에 JVM 안에서 띄울 수 있다.

그래서 test 경로의 application.yml 에서 url 경로를 메모리 모드로 띄울 수 있다. 

https://www.h2database.com/html/cheatSheet.html

spring:
  datasource:
    url: jdbc:h2:mem:test
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
#        show_sql: true
        format_sql: true

그러나 application.yml 에서 위의 코드가 없어서 상관 없다. spring boot가 default로 memory 모드로 돌리기 때문이다.