필드에 @Autowired 주입 처럼 편하게 사용하는 방법을 제공하는 lombok 라이브러리가 있다.
2. build.gradle 파일을 수정한다.
/build.gradle
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
3. Plugins 를 확인한다.
Preferences -> plugins 검색 -> lombok 검색 -> installed 에 있는지 확인 ( 없으면 설치 )
4. Annotation processors 를 설정한다.
5. @Getter @Setter Annotation을 확인해본다.
package hello.core;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("teepo");
System.out.println(helloLombok.getName());
}
}
이렇게 따로 Getter, Setter 를 선언해주지 않아도 사용할 수 있다.
String에서도 유용한 상황이 있는데,
예를 들어 아래의 코드가 있을 때
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// test
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
변수 선언에 final 을 붙이고 생성자를 Annotation으로 만들어 줄 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// test
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
스프링 컨테이너는 지저분한 코드가 들어가지 않아도 된다.
3. 싱글톤 패턴의 주의점
여러 클라이언트가 하나의 같은 객체를 공유하기때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
특정 클라이언트에 의존적인 필드가 있으면 안된다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
가급적 읽기만 가능해야 한다.
변수를 따로 할당해주어서 쓰거나 해야지 직접 값을 변경하는 방법은 좋지 않다.
4. @Configration , 싱글톤
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
이런 식의 코드가 있을 때 일반적인 생각으로는 AppConfig 파일이 불러와질 때 memberRepository는 3번이 불러와져야 한다.
하지만 스프링이 싱글톤 패턴을 적용시켜서 한 번만 불러와진다.
5. 바이트코드 조작법
AppConfig 를 로그로 찍어보면 CGLIB이 붙은 것을 볼 수 있다.
자바 코드를 바이트 코드로 바꿔서 스프링 컨테이너에 등록을 하는 것을 알 수 있다.
바이트 코드를 비교해서 이미 등록이 된 bean 을 두 번 반환하지 않고 한 번만 반환하는 것을 알 수 있다.
이는 @Configuration Annotation 을 사용해야지만 바이트 코드를 사용한 싱글톤이 보장됨을 의미한다.
ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다. create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다. 해보자.
3. JPA 엔티티 매핑
/com.example.demo/domain/Member
package com.example.demo.domain;
import javax.persistence.*;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4. JpaMemberRepository 생성
/repository/JpaMemberRepository
package com.example.demo.repository;
import com.example.demo.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return null;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
5. 서비스 계층에 트랜잭션 추가
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class MemberService {
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.