기본값은 true이며, 이 경우 스프링이 @Configuration 클래스를 CGLIB 프록시로 감싸서 메서드 호출을 가로채고, 항상 싱글톤 빈을 반환합니다.

하지만 false로 설정하면 프록시를 사용하지 않으며, 각 @Bean 메서드가 직접 호출되어 빈을 생성합니다. 다음과 같이 예제를 통해 상세 내용을 갈음하고자 한다.

// helloService()와 worldService()는 서로 의존하지 않기 때문에 proxyBeanMethods = false로 해도 문제 없습니다.
// 이럴 땐 성능 면에서 약간 이득을 볼 수 있습니다. (프록시 미사용)
@Configuration(proxyBeanMethods = false)
public class SimpleConfiguration {

    @Bean
    public HelloService helloService() {
        return new HelloService();
    }

    @Bean
    public WorldService worldService() {
        return new WorldService();
    }
}

// proxyBeanMethods = true 인 경우
// proxyBeanMethods = true라서 repository()가 여러 번 호출되어도 동일한 인스턴스를 리턴함
// Spring이 내부적으로 프록시를 사용해서 해당 메서드 호출을 컨테이너 호출로 변환
@Configuration
public class ServiceConfiguration {

    @Bean
    public Repository repository() {
        return new Repository();
    }

    @Bean
    public Service service() {
        return new Service(repository()); // 프록시로 가로채서 싱글톤 보장됨
    }
}

// 아래와 같이 하면 repository()가 직접 호출되어 다른 인스턴스가 생성됩니다.
// Service는 실제로 스프링 빈이 아닌, 새롭게 만들어진 Repository를 참조합니다.
@Configuration(proxyBeanMethods = false)
public class WrongConfiguration {

    @Bean
    public Repository repository() {
        return new Repository();
    }

    @Bean
    public Service service() {
        return new Service(repository()); // 직접 호출되므로 새 인스턴스!
    }
}

// 이 방식은 가장 안정적이고, 성능까지 고려한 현대적인 스프링 방식입니다.
// 생성자 주입이나 메서드 인자 주입을 적극적으로 활용하세요.
@Configuration(proxyBeanMethods = false)
public class SafeConfiguration {

    @Bean
    public Repository repository() {
        return new Repository();
    }

    @Bean
    public Service service(Repository repository) {
        return new Service(repository); // 스프링이 주입하므로 안전
    }
}
반응형

 

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(MockitoExtension.class)
@SpringBootTest
public class SampleServiceTest {

    @Autowired
    private MasterService masterService;

    @Mock
    private SlaveService slaveService;

    @Test
    void test() throws IOException {
        when(slaveService.checkPrint(any())).thenReturn(null);
        masterService.path(null);
    }
}

 

@SpringBootTest는 스프링 컨텍스트를 로드하면서 실제 빈을 주입한다. 때문에 SlaveService는 @Mock으로 선언되어있지만 실제 MasterService는 스프링컨테스트에서 실제빈을 주입받아 처리하기에 SlaveService의 Mock객체가 MasterService에 주입되지 않고, 실제 빈이 사용된다.

 

해결 방법으로는 크게 두가지 이다.

 

1. @InjectMocks 사용

@InjectMocks를 사용하면 @Mock으로 사용한 선언된 객체들이 MasterService에 자동으로 주입된다. 

 

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(MockitoExtension.class)
@SpringBootTest
public class SampleServiceTest {

    @InjectMocks
    private MasterService masterService;	// mock 객체가 주입됨

    @Mock
    private SlaveService slaveService;

    @Test
    void test() throws IOException {
        when(slaveService.checkPrint(any())).thenReturn(null);
        masterService.path(null);
    }
}

 

2. @MockBean 사용

@MockBean을 사용하면 SpringBootTest가 실행될 때 slaveService의 실제 빈 대신 Mock객체가 주입된다.

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(MockitoExtension.class)
@SpringBootTest
public class SampleServiceTest {

    @Autowired
    private MasterService masterService;

    @MockBean
    private SlaveService slaveService;

    @Test
    void test() throws IOException {
        when(slaveService.checkPrint(any())).thenReturn(null);
        masterService.path(null);
    }
}

 

차이점 요약

 

주입방식 MasterService 객체 SlaveService 객체
@InjectMocks + @Mock 실제 객체 (테스트용으로 생성) Mock 객체
@Autowired + @MockBean 스프링 빈 (실제 빈) Mock 객체 (스프링 컨텍스트 내에서 등록됨)

 

따라서,

  • 단위 테스트를 할 때는 @InjectMocks를 사용하여 빠르게 테스트
  • 스프링 컨텍스트와 함께 통합 테스트를 할 때는 @MockBean을 사용하여 주입
반응형

테이블을 설계하다 보면 자기 자신을 참조하는 경우가 있다. 이를 JPA로 나타내자면 아래와 같다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    @Builder.Default
    @OneToMany(mappedBy = "parent")
    private Set<Category> children = new HashSet<>();
}

 

 

이와 다른 형태로는 아래와 같은데 이와 다르게 자식 테이블이 별도로 생성되어 이 경우 불필요한 join query가 불필요하게 발생되어 아래와 같은 방식 보단 첫번째 방식으로 계층형 테이블을 설계해야 한다

 

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany
    private Set<Category> children = new HashSet<>();
}

 

반응형

SQL과 객체지향언어가 갖는 큰 차이점 중 하나가 바로 상속의 유무이다.

객체 지향언어는 상속을 지원하지만, 데이터베이스는 상속을 지원하지 않는다. 

 

스프링에서는 JPA를 이용하여 ORM 기술을 사용하는데, 클래스 간 상속 관계에 대해서는 어떻게 정의를 하는지 알아보자.

 

물리 모델을 데이터 베이스 테이블로 구현하는 방법에는 크게 4가지 방법이 존재한다.

 

- MappedSupperclass

- Single Table 전략

- Joined Table 전략

- Table per Class 전략

 

MappedSuperclass 

 

MappedSuperclass 어노테이션은 부모클래스를 데이터베이스의 테이블로 매핑하지 않고 자식클래스에게 매핑 정보를 제공하고 싶을 때만 사용한다.

 

특징

- MappedSuperclass 어노테이션을 사용한 클래스는 엔티티가 아니다. 따라서 해당 클래스를 가지고 영속성 컨텍스트 또는 JPQL 등에서 사용할 수 없다.

- MappedSuperclass 어노테이션을 사용한 클래스는 자체적으로 의미가 없기에 추상 클래스로 만드는 것이 좋다.

 

Single Table 전략

 

Single Table 전략은 각 클래스 계층에 대해 하나의 테이블을 만들며 명시적으로 지정하지 않는 경우 JPA에서는 Default로 해당 전략을 사용한다. 

discriminatorType으로 식별자를 integer로 할 것 인지 혹은 다른 타입으로 지정할 것인지 선택할 수 있다. name을 지정하게 되면 해당 식별자의 이름이 지정되어 테이블로 들어간다. 

 

특징

- 싱글 테이블이기 때문에 조인 쿼리를 사용하지 않고 한 번에 조회가 가능하다.

- 싱글 테이블이기 때문에 null 값이 들어가는 자료가 생긴다.

- 단일 테이블로 유지하기 때문에 테이블의 크기가 커진다.

 

Joined Table 전략

 

조인 전략은 계층 구조를 띄는 각 클래스가 해당 테이블로 매핑된다. 이 때, 테이블마다 반복되는 열이 하나가 있는 데, 이는 조인할 때 반드시 필요한 식별자 이다. 

 

특징

- 테이블을 정규화 할 수 있다.

- 저장공간을 효율적으로 사용한다 이는 정규화를 실시했기 때문이다.

- 조회할 때 외래키를 이용하여 조인 쿼리를 사용하기 때문에 성능이 좋지 않다.

- 데이터 등록 시 쿼리가 한 번 더 발생한다. 

 

Table per Class

 

Table per Class 는 각각의 테이블을 모두 생성하는 전략이다. 결과적으로 MappedSuperclass를 사용한 것돠 유사하다. 그러나 Table per Class 전략에서는 상위 클래스에 대한 엔티티를 정의하기 때문에 상위 클래스를 연결 및 쿼리 사용이 가능하다.

 

특징

- 서브 타입과 구분해서 처리할 때 효과적이다.

- 상속 없이 각 엔티티를 매핑하는 것과 다르지 않아, Union을 사용하여 쿼리를 작성한다.

- Union 쿼리로 인해 성능이 저하될 수 있다.

반응형

Lombok 에서 'is' prefix가 붙은 컬럼의 boolean과 Boolean의 타입 차에 따라 직렬화시 값이 달라진다. 

 

@Getter
@Setter
public class Member {

    private boolean isDisabled;

    public void test(){
        this.setDisabled(true);  //Lombok - 자동생성된 setter
        this.isDisabled();       //Lombok - 자동생성된 getter
    }
}

@Getter
@Setter
public class Member {

    private Boolean isDisabled;

    public void test(){
        this.setIsDisabled(true);  //Lombok - 자동생성된 setter
        this.getIsDisabled();      //Lombok - 자동생성된 getter
    }
}

 

위와 같이 객체의 속성이 boolean 타입에 'is' prefix를 사용하게 될 경우, getter가 isXXXX로 생성되기 때문에 정상적인 동작이 이루어지지 않는다 때문에 아래처럼 @JsonProperty의 is를 제거한 상태로 넣거나 getIsXXXX() getter() 메서드를 작성하면 정상 동작한다.

@Getter
@Setter
public class Member {

	@JsonProperty("disabled")
    private boolean isDisabled;

    public void test(){
        this.setDisabled(true);  //Lombok - 자동생성된 setter
    }
}

 

다른 방법은 타입을 Warpper class인  Boolean으로 사용하여 getter, setter를 생성하면된다.

반응형

Springboot2로 운영되는 서비스를 Springboot3 로 버전 업을 하게 되었을 시에 javax 대부분의 패키지가 jakarta 패키지로 이관되어 기존 QueryDSL의 JPAQueryFactory는 javax.persistence 패키지를 참조하고 있어 호환이 불가능 하기에 아래처럼 수정하면 에러가 해결된다.

// build.gradle

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

 

 

반응형

'SpringBoot > JPA' 카테고리의 다른 글

계층형 테이블 구현  (1) 2024.10.09
Inheritance 전략  (0) 2024.10.08
값 타입과 값 타입 컬렉션에 관한 사용법  (0) 2023.04.07

루트 프로젝트 생성 후 해당 프로젝트의 우클릭을 통해 [New] - [Module] 선택 후 Spring Initializr를 통해 새로운 모듈을 생성한 후 루트 프로젝트의 settings.gradle.kts에 멀티 모듈을 include 시킨다.

// settings.gradle.kts
rootProject.name = "bmt-backend"

include(":member-admin")

그 후 부모 프로젝트를 빌드 시키면 

위와 같이 멀티 모듈을 작성할 수 있다. 

 

반응형

값 타입인 경우

@Entity
public class DeliveryJpaEntity {
    ...
    
    @Embedded	// 생략 가능
    private AddressJpaEntity address;
    
    ...
}

@Embeddable
public class AddressJpaEntity {

    private String address;

    private String ji;

    private String bun;
}

 

값 타입 컬렉션인 경우

- 값 타입을 컬렉션에 담아서 쓰는 걸 값 타입 컬렉션이라 한다.

- 값 타입 컬렉션은 영속성 전이(Casecade)와 고아 객체 제거(Orphan remove)가 Default 이다.

@Entity
public class DeliveryJpaEntity {

    ...
    
    @ElementCollection	// 기본 fetch 전략은 lazy 이다.
    @CollectionTable(name = "delivery_item", joinColumns = @JoinColumn(name = "delivery_id"))
    private List<DeliveryItemJpaEntity> deliveryItems = new ArrayList<>();
    
    ...
}

@Embeddable
public class DeliveryItemJpaEntity {

    private String name;

    private Long quantity;

    private BigDecimal amount;
}

 

값 타입 컬렉션의 한계

엔티티는 ID로 DB에서 찾을 수 있기 때문에 쉽게 CRUD가 가능하나, 값 타입은 식별자라는 개념이 없기에 원본데이터를 찾기 어렵다, 값 타입 컬렉션은 별도의 테이블에 저장되는데 이 테이블 안에 있는 값타입의 값이 변경 되면 기존 값 타입 컬렉션을 제거하고 컬렉션 전체를 새롭게 저장하는 방식으로 이뤄진다. 때문에 컬렉션의 양이 많아지면 쿼리 수가 많아지기 때문에 컬렉션의 데이터가 많으면 값 타입 컬렉션 대신 일대다 관계 엔티티로 수정하는 것을 고려해야 한다.

반응형

'SpringBoot > JPA' 카테고리의 다른 글

계층형 테이블 구현  (1) 2024.10.09
Inheritance 전략  (0) 2024.10.08
Springboo3 으로 변경 했을시에 QueryDSL 변경 점  (0) 2023.06.09

+ Recent posts