"인프런 - 실전! 스프링 부트와 JPA 활용! 강의를 듣고 작성한 글 입니다."
www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1#
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다. 초급
www.inflearn.com
* JPA 를 활용할 때 가장 중요하게 생각해야 하는 것?
: JPA 의 모든 데이터 변경과 같은 로직들은 가급적이면 트랜잭션 내부에서 수행되어야 하기 때문에 데이터의 변경에 대한 기능이 수행되는 경우 @Transactional 어노테이션을 사용하는 것을 항상 기억하자.
@Transactional 어노테이션
@Transactional 어노테이션은 2가지 종류가 있는데, 하나는 스프링에서 제공하는 것과 다른 하나는 javax 에서 제공하는 것이다.
그런데 이미 스프링을 사용하는 것에서 부터 스프링에 의존적인 로직을 많이 사용하기 때문에 되도록이면 javax 보다는 스프링에서 제공하는 @Transactional 어노테이션을 사용하는 것이 좋다.(그래야 사용할 수 있는 옵션들의 폭이 늘어난다.)
import org.springframework.transaction.annotation.Transactional;
@Transactional 어노테이션을 사용할 때 데이터의 변경이 아닌 단순 조회의 경우 readOnly 라는 옵션을 사용할 수 있다.
위의 옵션을 설정해주면 JPA 가 조회에 있어서 성능을 좀 더 최적화 해준다.
(디테일하게 얘기하자면 영속성 컨텍스트를 플러시 안 하거나, 더티 체킹을 하지 않는 등의 이점, 추가로 데이터베이스에 따라서는 읽기 전용의 트랜잭션이 감지되면 리소스를 괜히 더 많이 사용할 필요 없이 읽기 모드등을 활용해서 읽게끔하는 드라이브 또한 존재한다고 한다. - 자세한건 데이터베이스를 쓸 때 확인해봐야 함)
결론적으로 말하자면 단순 데이터 조회와 같은 읽기 기능의 경우 @Transactional 어노테이션에서 readOnly = true 와 같은 옵션을 지정해주면 된다.
반대로 읽기가 아닌 쓰기 기능의 경우 위의 옵션을 지정해서는 안된다.
데이터를 변경시켜야 하는 기능에서 위의 옵션을 활용하게 될 경우 데이터가 변경되지 않는 불상사가 발생하게 된다.
- MemberService.java
@Service // @Repository 어노테이션과 같이 내부에 @Component 어노테이션이 선언되어 있다.
@Transactional(readOnly = true) // 기본적으로는 읽기 전용의 트랜잭션을 생성한다.
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional // 따로 트랜잭션을 생성한 경우 위의 읽기 전용 옵션보다 우선하여 기능이 수행된다.(readOnly = false)
public Long join(Member memeber) {
validateDuplicateMember(member); // 유효성 검증 메소드
memberRepository.save(member);
return member.getId();
}
}
* 유효성 검증의 경우 아래의 형태로 메소드를 작성했는데 이 방식의 경우 문제가 발생할 수 있다.
- MemberService.java
// 이름이 중복되는 회원이 있는지 검증하는 메소드
private void validateDuplicateMember(Member member) {
// 검증결과 문제가 있을 경우 Exception 을 터트린다.
List<Member> findMembers = memberRepository.findByName(member.getName());
if(!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
- MemberRepository.java
public List<Member> findByName(String name){
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
위와 같이 유효성 검증 메소드를 작성할 경우, 만약 똑같은 이름을 가진 사람이 정확히 동시에 정보를 저장해서 유효성 검증 메소드를 호출하게 되면, 둘이 동시에 유효성 검증을 통과해서 save 메소드를 통해 정보를 저장시킬 수 있다.
그렇기 때문에 비즈니스 로직이 위와 같은 경우가 있다고 하더라도 실무에서는 최후의 방어를 해두는 것이 좋다.
멀티 스레드와 같은 상황을 고려해서 데이터베이스의 멤버의 이름을 유니크 제약조건으로 잡아주는 등의 방법이 있을 것이다.
@Autowired 어노테이션
@Autowired 어노테이션을 활용하면, 스프링이 스프링 빈에 등록되어 있는 MemberRepository 와 같은 클래스를 주입(injection) 해준다.(필드 인젝션)
아래와 같은 코드를 한번 보자.
- MemberService.java
@Autowired
private MemberRepository memberRepository;
위와 같은 방식으로 코드를 작성하면 단점이 있다.
이처럼 스프링 빈에 있는 클래스를 주입해두면 테스트를 할 때 필드를 바꿔야 할 대가 있는데, private 으로 되어 있기 때문에 해당 필드에 쉽게 접근할 수가 없다.
이와 같은 문제를 해결하려면 아래와 같은 방법을 쓸 수 있다.
- MemberService.java
private MemberRepository memberRepository;
@Autowired // setter 활용
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
* Setter Injection
: 스프링이 필드로 바로 주입을 해주는 것이 아니라, setter 를 통해 들어와서 주입해준다.
이 방식의 장점은 테스트 코드를 작성할 때 Mock 과 같은 것을 중간에 주입해 줄 수 있다는 점이다.
즉, Setter 를 통해 주입을 해주기 전, private 선언을 통해 가짜 클래스를 먼저 주입해주는 방식이다.
- 이 방식의 단점?
보통 애플리케이션 로딩 시점에 이미 조립이 다 끝나기 때문에 중간에 변경 시킨다거나 할 수가 없다.
즉, 런타임에(실제 애플리케이션이 돌아가는 시점에) 누군가가 주입해준 클래스를 변경키실 수가 없다는 것이다.
-> 사실 Setter 를 활용하는 방식도 좋지 않음
궁극적으로 권장하는 클래스 주입(Injection) 방식?
생성자 Injection 을 활용한다.
- MemberService.java
private MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
위와 같이 코드를 작성하면 스프링이 생성자에서 해당 클래스를 주입해준다.
이렇게 해두면 생성자를 통해 주입하고자 하는 클래스가 완성되기 때문에 중간에 Setter 와 같은 메소드를 통해 주입해준 클래스를 변경시키거나 할 수 없다.
또한 테스트를 할 때 아래와 같은 방식으로 코드를 작성한다고 해보자.
public static void main(String[] args) {
MemberService memberService = new MemberService();
}
이와 같이 코드를 작성하면 new 키워드를 통해 객체를 생성할 때 생성자로 인해 오류가 발생하기에, 테스트 케이스를 작성할 때 Mock 과 같은 것을 주입해 주어야 한다는 것을 직관적으로 알 수 있게 된다.
하지만 생성자를 통해 스프링 빈에 있는 클래스를 주입해준다고 해도 뭔가 번거롭다.
그렇기 때문에 코드를 아래와 같이 작성해주어도 무방하다.
- MemberService.java
private MemberRepository memberRepository;
// @Autowired 어노테이션 제거
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
요즘 나온 스프링은 생성자가 하나만 있는 경우 @Autowired 어노테이션이 없어도 자동으로 클래스를 주입시켜 준다.
이와 같이 코드를 작성하게 되면 중간에 데이터를 변경시킬 일이 없기 때문에 private 으로 가짜 필드를 생성할 때 final 키워드를 붙여주는 것을 권장한다.
- MemberService.java
private final MemberRepository memberRepository;
그렇다면 여기서 lombok 을 한 번 적용해보자.
@AllArgsConstructor 어노테이션
@AllArgsConstructor 어노테이션
: 생성자를 굳이 만들지 않아도 작성되어 있는 필드를 보고 위와 똑같은 생성자를 자동으로 만들어준다.
위의 어노테이션을 지정해주면 굳이 생성자 코드를 작성하지 않아도 된다.
그런데 이것보다 더 나은 선택지가 있다.
@RequiredArgsConstructor 어노테이션
@RequiredArgsConstructor 어노테이션
: final 키워드가 붙은 필드들만 지정해서 생성자를 만들어준다.
* 여기서 또 하나 재밌는 점?
그동안 MemberService 클래스에 주입해줬던 MemberRepository 클래스 또한 생성자를 통해 클래스 Injection 을 해 줄 수 있다.(스프링이 지원해준다.)
- MemberRepository.java
@PersistenceContext
private EntityManager em;
public MemberRepository(EntityManager em) {
this.em = em;
}
그런데 여기서 만약 스프링 부트의 Spring Data JPA 를 쓴다면 코드를 아래와 같이 작성해주는 것도 가능하다.
- MemberRepository.java
@Autowired
private EntityManager em;
public MemberRepository(EntityManager em){
this.em = em;
}
-> 원래 EntityManager 의 경우 @Autowired 어노테이션 으로는 불가능하고, @PersistenceContext 어노테이션을 사용해야 클래스 주입이 가능하다.(표준 어노테이션임)
다만 스프링 부트가 @Autowired 어노테이션 또한 클래스 주입이 가능하도록 지원을 해준다.
그렇다면 이 클래스 또한 MemberService 에서 했던것과 같은 방식으로 코드를 작성해 줄 수 있다.
- MemberRepository.java
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
.......
}
'Spring boot' 카테고리의 다른 글
스프링 부트 - 주문 및 주문 상품 Entity 개발 학습내용 (0) | 2021.01.16 |
---|---|
스프링 부트 - 회원 기능 테스트 학습내용 (0) | 2021.01.07 |
스프링 부트 - 회원 Repository 개발 학습내용 (0) | 2021.01.05 |
스프링 부트 - Entity 설계 시 주의할 점(2) (0) | 2020.12.28 |
스프링 부트 - Entity 설계 시 주의할 점(1) (0) | 2020.12.28 |