Spring MVC에서 응답을 쓰는 방식이 크게 두가지가 있다.

1. response.getWriter() - 문자 기반 응답

2. response.getOutputStream() - 바이트 기반 응답

 

한 번 getWriter()를 호출하면 내부적으로 OutputStream이 이미 사용된 것으로 간주되며, 그 이후에는 getOutputStream()을 사용할 수 없습니다, 반대도 마찬가지입니다.

 

GenericFilterBean만 상속받고 있다면, 다음과 같은 흐름에서는 여러 번 필터가 실행될 수 있습니다.

  • 예외 처리 중 /error로 forward
  • Servlet Dispatcher가 다시 요청 처리

이와 반대로 

OncePerRequestFilter가 요청당 한 번만 필터를 실행하도록 자동으로 보장해 주는데, 내부에 그런 "중복 실행 방지 로직"이 구현되어 있기 때문이다.

 

때문에 OncePerRequestFilter는 이런 경우에 유용하다. 

  • 예외 처리로 인해 /error로 내부 forward 되는 경우
  • DispatcherType.FORWARD, DispatcherType.ERROR로 필터 체인이 재실행되는 경우
  • 보안 필터(JWT, 인증, IP 제한 등)처럼 요청당 한 번만 실행돼야 할 때

-> 참고로 OncePerRequestFilter는 GenericFilterBean을 상속받고 있다.

 

OncePerRequestFilter에는 ShouldNotFilter라는 메소드가 존재하며 이를통해 filter를 안타게끔 패스 지정을 할 수 있다.(REGEX 지정 가능)

반응형

'SpringBoot' 카테고리의 다른 글

Lombok getter  (0) 2024.10.07

기본값은 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를 생성하면된다.

반응형

'SpringBoot' 카테고리의 다른 글

GenericFilterBean과 OncePerRequestFilter의 차이  (0) 2025.06.23

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")

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

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

 

반응형

+ Recent posts