네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'web'에 해당되는 글 82건


반응형

The Scheduled Annotation in Spring

@Scheduler 를 사용해서 일정한 시간 간격으로, 혹은 특정 일정에 코드가 실행되도록 해보자.

 

Spring Scheduler

Dependency

spring-boot-starter에 기본적으로 의존 org.springframework.scheduling

Enable Scheduling 

- Project Application Class에 @EnableScheduling 추가

@EnableScheduling // 추가
@SpringBootApplication
public class ScheduledrApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduledrApplication.class, args);
    }
}

- scheduler를 사용할 클래스에 @Component, 메서드에 @Scheduled 추가

 

[@Scheduled 규칙]

  • 메서드는 void 타입으로
  • 메서드는 매개변수 사용 불가

Example

fixedDelay

- 해당 메서드가 끝나는 시간 기준, milliseconds 간격으로 실행

- 하나의 인스턴스만 항상 실행되도록 해야 할 상황에서 유용

@Scheduled(fixedDelay = 1000)
// @Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds}") // 문자열 milliseconds 사용 시
public void scheduledFixedDelayTask() throws InterruptedException {
    log.info("Fixed delay task - {}", System.currentTimeMillis() / 1000);
    Thread.sleep(5000);
}

 

fixedRate

- 해당 메서드가 시작하는 시간 기준 milliseconds 간격으로 실행

- 병렬로 Scheduler를 사용할 경우, 클래스에 @EnableAsync, 메서드에 @Async 추가

- 모든 실행이 독립적인 경우에 유용

@Async
@Scheduled(fixedRate = 1000)
// @Scheduled(fixedRateString = "${fixedDelay.in.milliseconds}") // 문자열 milliseconds 사용 시
public void scheduledFixedRateTask() throws InterruptedException {
    log.info("Fixed rate task - {}", System.currentTimeMillis() / 1000);
    Thread.sleep(5000);
}

fixedDelay + fixedRate

- initialDelay 값 이후 처음 실행되고, fixedDelay 값에 따라 계속 실행

@Scheduled(fixedDelay = 1000, initialDelay = 5000) 
public void scheduleFixedRateWithInitialDelayTask() { 
    long now = System.currentTimeMillis() / 1000; 
    log.info("Fixed rate task with one second initial delay - {}", now); 
}

Cron

- 작업 예약으로 실행

@Scheduled(cron = "0 15 10 15 * ?") // 매월 15일 오전 10시 15분에 실행
// @Scheduled(cron = "0 15 10 15 11 ?") // 11월 15일 오전 10시 15분에 실행
// @Scheduled(cron = "${cron.expression}")
// @Scheduled(cron = "0 15 10 15 * ?", zone = "Europe/Paris") // timezone 설정
public void scheduleTaskUsingCronExpression() {
    long now = System.currentTimeMillis() / 1000;
    log.info("scheduled tasks using cron jobs - {}", now);
}

 

Setting Information

fixedDelay

- 이전 작업이 종료된 후 설정 시간(milliseconds) 이후에 다시 시작

- 이전 작업이 완료될 때까지 대기

fixedDelayString 

- fixedDelay와 동일, 설정 시간(milliseconds)을 문자로 입력하는 경우에 사용

fixedRate

- 고정 시간 간격으로 시작

- 이전 작업이 완료될 때까지 다음 작업이 진행되지 않음

- 병렬 동작을 사용할 경우 @Async 추가

fixedRateString

- fixedRate와 동일, 설정시간을 문자로 입력하는 경우에 사용

initialDelay

- 설정된 initialDelay 시간 후부터 fixedDelay 시간 간격으로 실행

initialDelayString

- initialDelay와 동일, 설정시간을 문자로 입력하는 경우에 사용

cron

- cron 표현식을 사용한 작업 예약

- 첫번째부터 초(0-59), 분(0-59), 시간(0-23), 일(1-31), 월(1-12), 요일(0-7)

zone

- 미설정시 Local 시간대 사용

 

 

 

참고

https://www.baeldung.com/spring-scheduled-tasks

https://data-make.tistory.com/699

 

[Spring Boot] Scheduler 사용해보기(일정 주기로 실행하는 스프링 스케쥴러)

The Scheduled Annotation in Spring @Scheduler 를 사용해서 일정한 시간 간격으로, 혹은 특정 일정에 코드가 실행되도록 해보자. Spring Scheduler Dependency Spring Boot starter 에 기본적으로 의존 org.spri..

data-make.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

스프링 빈 생명주기 콜백

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

빈 생명주기 콜백은 스프링 빈이 생성된 후 의존관계 주입이 완료되거나 죽기 직전에 스프링 빈 안에 있는 메서드를 호출해주는 기능이다.

 

스프링 빈의 이벤트 사이클

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료

- 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출

- 소멸전 콜백: 빈이 소멸되기 직전에 호출

 

스프링 빈은 크게 3가지 방법으로 빈 생명주기 콜백을 지원

- 인터페이스 (InitializingBean, DisposableBean)

- 설정정보 초기화 메서드, 종료 메서드 지정

- @PostConstruct, @PreDestroy

 

@PostConstruct, @PreDestroy

@PostConstruct
public void init() {
    System.out.println("초기화 메서드 호출");
}

@PreDestroy
public void destroy() {
    System.out.println("종료 메서드 호출");
}

- 패키지는 javax.annotation.PostConstruct이다. 스프링에 종속적인 기술이 아니라 JSR-250 표준이기 때문에 스프링이 아닌 다른 컨테이너에서도 동작한다.

- 컴포넌트 스캔과 잘 어울린다.

- 외부 라이브러리에는 적용하지 못한다. 외부 라이브러리를 초기화, 종료를 해야 할 경우, @Bean(initMethod="", destroyMethod="")를 사용하자.

 

@PostConstruct

- 객체의 초기화 단계

- 객체가 생성된 후, 별도의 초기화 작업을 위해 실행하는 메서드에 선언한다.

- @PostConstruct 어노테이션을 설정해놓은 Init 메서드는 WAS가 띄워질 때 실행된다.

 

@PreDestroy

- 객체 소멸 단계 직전

- 스프링 컨테이너에서 객체(빈)를 제거하기 전에 해야할 작업이 있다면 실행할 메서드에 선언한다.

- close() (AbstractApplicationContext) context.close() 하기 직전에 실행된다.

 

InitializingBean, DisposableBean 인터페이스

각각 afterPropertiesSet(), destroy()를 지원한다.

- afterPropertiesSet(): 의존관계 주입이 끝난 후 호출

- destroy(): 빈이 죽기 직전에 호출

 

* 단점

- 이 인터페이스는 스프링 전용 인터페이스이기 때문에 스프링에 의존한다.

- 초기화, 소멸 메서드의 이름을 변경할 수 없다.

- 외부 라이브러리에 적용할 수 없다.

 

빈 초기화, 소멸 메서드 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod="close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

 

메서드 이름을 자유롭게 쓸 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다.

설정 정보를 사용하기 때문에 외부 라이브러리에도 초기화, 종료 메서드를 사용할 수 있다.

 

추론

@Bean의 destroyMethod 속성에는 특별한 기능인 추론이 있다.

라이브러리는 대부분 close, shutdown이라는 이름의 종료 메서드를 사용한다.

@Bean의 destroyMethod는 기본값이 inferred 추론으로 되어있다.

이 추론 기능은 close, shutdown이란 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출해준다.

따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다. 추론 기능을 사용하기 싫으면 destroyMethod=""처럼 공백을 지정하면 된다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

JPA를 사용하다 보면 N+1의 문제에 마주치고 Fetch Join을 접하게 된다.

일단 Join으로 N+1을 해결하지 못하는지와 Fetch Join과 일반 Join의 차이점을 정리해보자.

 

Join, Fetch Join 차이점

[ 일반 Join ]

- Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화

- 조회의 주체가 되는 Entity만 SELECT 해서 영속화하기 때문에 데이터는 필요하지 않지만 연관 Entity가 검색 조건에는 필요한 경우에 주로 사용한다.

[ Fetch Join ]

- 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화

- Fetch Join이 걸린 Entity 모두 영속화하기 때문에 FetchType이 LAZY인 Entity를 참조하더라도 이미 영속성 컨텍스트 안에 들어있기 때문에 따로 쿼리가 실행되지 않은 채로 N+1 문제가 해결됨

 


확인 

Team.java

@Entity
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@ToString
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    @Builder.Default
    private List<Member> members = new ArrayList<>();
    
    public void addMember(Member member) {
        member.setTeam(this);
        members.add(member);
    }
}

Member.java

@Entity
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@ToString
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private int age;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}

 

일반 Join을 이용

// TeamRepository.java
@Query("SELECT distinct t FROM Team t JOIN t.members")
public List<Team> findAllWithMembersUsingJoin();
Hibernate:
    select
         distinct team0_.id as id1_1_,
         team0_.name as name2_1_
    from team team0_ 
    inner join
        member members1_
            on team0_.id=members1_.team_id

Team과 Member가 Join 된 형태의 쿼리가 실행됩니다. 하지만 가져오는 컬럼들은 Team의 컬럼인 id와 name만 가져오고 있다.

쿼리를 보면 분명 Join을 했는데 각 Team의 Lazy Entity인 members가 아직 초기화되지 않았다. 

실제로 일반 Join은 실제 쿼리에 Join을 걸어주기는 하지만 Join 대상에 대한 영속성까지는 관여하지 않는다.

 

Fetch Join을 이용한 N+1 해결

// TeamRepository.java
@Query("SELECT distinct t FROM Team t JOIN FETCH t.members")
public List<Team> findAllWithMembersUsingFetchJoin();
Hibernate:
    select
         distinct team0_.id as id1_1_,
         members1_.id as id1_0_1_,
         team0_.name as name2_1_,
         members1_.age as age2_0_1,
         members1_.name as name3_0_1_,
         members1_.team_id as team_id4_0_1_,
         members1_.team_id as team_id4_0_1_,
         members1_.id as id1_0_1_
    from team team0_ 
    inner join
        member members1_
            on team0_.id=members1_.team_id

일반 Join 과 Join의 형태는 같지만 SELECT 하는 컬럼에서 차이가 있다. 

  • 일반 Join : join 조건을 제외하고 실제 질의하는 대상 Entity에 대한 컬럼만 SELECT
  • Fetch Join : 실제 질의하는 대상 Entity와 Fetch join이 걸려있는 Entity를 포함한 컬럼 함께 SELECT

일반 Join 사용처

무작정 Fetch Join을 사용해서 전부 영속성 컨텍스트에 올려서 쓰기보다는 일반 Join을 적절히 이용하여 필요한 Entity만 영속성 컨텍스트에 올려서 사용하는 것이 좋다.

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

[@Autowired 빈 탐색 전략과 @Qualifier, @Primary]

1. 빈(Bean) 등록과 조회 규칙

[ 빈 등록 ]

Spring은 기본적으로 메서드의 이름을 Bean의 이름으로 사용한다. 하지만 개발자가 직접 빈의 이름을 부여할 수도 있다.

@Bean
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

@Bean("fixDiscountPolicy")
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

 

[ 빈 조회 규칙 전략 ] 

@Autowired가 등록된 빈을 찾을 때에는 다음과 같은 매칭 규칙으로 빈을 조회한다.

  1. 주고받고자 하는 타입으로 매칭을 시도한다.
  2. 타입이 여러 개면 필드 또는 파라미터 이름으로 매칭을 시도한다.

 

하지만 빈의 이름이 충돌되어 빈 이름만으로 해결이 불가능한 경우나 빈에 추가 구분자나 우선순위를 부여하고 싶은 경우에 @Qualifier나 @Primary 어노테이션을 이용해 편리하게 해결할 수 있다.

 

추가로 Spring은 @Resource라는 어노테이션도 제공하고 있다. @Resource는 @Autowired와 달리 필드 이름으로 빈을 찾는다.

  • @Autowired: 필드 타입을 기준으로 빈을 찾음
  • @Resource: 필드 이름을 기준으로 빈을 찾음

 

2. @Qualifier와 @Primary

[ @Qualifier - 빈의 Alias(구분자) ]

빈의 이름만으로 부족하고, 추가적인 정보가 필요할 수 있다. 그런 상황에서 @Qualifier 어노테이션을 통해 빈에 추가 구분자를 붙여줄 수 있다.

@Qualifier("mainDiscountPolicy")
@Bean
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

다음과 같이 빈을 찾고자하는 경우에 @Qualifier 어노테이션을 부여하여 빈을 찾도록 할 수 있다.

public class DiscountServie {

    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public DiscountService(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}
  1. 해당 @Qualifier가 붙은 빈을 조회한다.
  2. @Qualifier가 붙은 빈을 찾지 못하면 필드명 또는 파라미터명으로 매칭을 시도한다. 
  3. 그래도 찾지못하면 NoSuchBeanDefinitionException 이 발생한다.

[ @Primary - 빈의 우선순위 부여 ]

여러 타입의 빈이 존재할 때, 특정 빈을 우선적으로 주입하도록 하고 싶다면 @Primary 어노테이션을 사용할 수 있다. Spring이 타입으로 비을 찾다가 Primary가 붙어있는 빈을 발견하면, 바로 해당 빈을 주입시킨다. 즉, @Primary는 여러 개의 빈들 중에 우선순위를 부여하는 방법이다.

@Primary
@Bean
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

만약 @Primary와 @Qualifier 모두 등록되어있다면 Qualifier가 우선순위를 갖는다.

 

[ 빈 충돌 발생 ]

빈 등록 시, 충돌이 발생한다면

  •  자동 빈 등록 vs 자동 빈 등록: 빈 이름 중복 에러가 발생한다.
  • 수동 빈 등록 vs 자동 빈 등록: 과거에는 수동 빈이 우선순위였지만 최근에는 에러가 발생한다.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

개발을 하다보면 API의 요청이나 응답을 처리할 때 또는 다른 계층으로 넘기는 파라미터가 너무 많은 시점에 별도의 DTO를 생성하는 것이 좋습니다.

 

[엔티티(Entity) 또는 도메인 객체(Domain Object)와 DTO를 분리해야 하는 이유]

1) 관심사의 분리

2) Validation 로직 및 불필요한 코드 등과의 분리

3) API 스펙과의 분리

4) API 스펙의 파악이 용이

 

1. 관심사의 분리

엔티티와 DTO를 분리해야 하는 가장 근본적인 이유는 관심사가 서로 다르기 때문이다. 관심사의 분리(separation of concerns, SoC)는 소프트웨어 분야의 오래된 원칙 중 하나로서, 서로 다른 관심사들을 분리하여 변경 가능성을 최소화하고, 유연하며 확장가능한 클린 아키텍처를 구축하도록 도와준다.

DTO(Data Transfer Object)의 핵심 관심사는 이름 그대로 데이터의 전달이다. DTO는 데이터를 담고, 다른 계층(Layer) 또는 다른 컴포넌트들로 데이터를 넘겨주기 위한 자료구조(Data Structure)이다. 그러므로 어떠한 기능 및 동작도 없어야 한다.

반면에 엔티티는 핵심 비즈니스 로직을 담는 비즈니스 도메인 영역의 일부이다. 그러므로 엔티티 또는 도메인 객체는 그에 따른 비즈니스 로직이 추가될 수 있다. 엔티티 또는 도메인 객체는 다른 계층이나 컴포넌트 사이에서 전달을 위해 사용되는 객체가 아니다.

엔티티와 DTO는 엄연히 서로 다른 관심사를 가지고 있고, 그렇기 때문에 분리하는 것이 합리적이다.

 

2. Validation 로직 및 불필요한 코드 등과의 분리

Spring 에서는 요청에 대한 데이터를 검증하기 위해 @Valid 어노테이션을 지원하고 있다.

@Valid 처리를 위해서는 @NotNull, @NotEmpty, @Size 등과 같은 유효성 검증 어노테이션들을 변수에 붙여주어야 한다. 반면에 JPA도 변수에 @Id, @Column 등과 같은 어노테이션을 활용해 객체와 관계형 데이터베이스를 매핑해주는데, DTO와 엔티티를 분리하지 않는다면 엔티티의 코드가 상당히 복잡해진다. 

또한, 엔티티 클래스의 생성일자, 수정일자를 나타내는 변수들은 API 요청이나 응답에서는 필요가 없다. 그렇기 때문에 응답에서 해당 변수를 제거하기 위해서는 @JsonIgnore 등과 같은 또 다른 어노테이션을 통해 별도의 작업이 필요할 수도 있다.

이렇게 엔티티 클래스를 사용하게 되면 핵심 비즈니스 도메인 코드들이 아닌 요청/응답을 위한 값, 유효성 검증을 위한 코드 등이 추가되면서 엔티티 클래스가 지나치게 비대해질 것이고, 확장 및 유지보수 등에서 매우 어려움을 겪게 될 것이다. 

그러므로 이러한 경우에는 별도의 DTO를 생성해서 엔티티로부터 분리하는 것이 바람직할 것이다.

 

3. API 스펙의 유지

@Entity 
@Table 
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Membership {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    private Long id;
    
    @Enumerated(EnumType.STRING)
    private MembershipType membershipType; 
    
    @Column(nullable = false) 
    private String userId;
    
    @Setter 
    @Column(nullable = false) 
    @ColumnDefault("0") 
    private Integer point;
}
{ 
    "id" : "15", 
    "membershipType" : "NAVER", 
    "userId" : "NC10523", 
    "point" : "10000" 
}

내부 정책 변경으로 userId를 memberId로 변경해야 하는 상황이다. DTO를 사용하지 않는다면 userId가 memberId로 바꿈에 따라 API 스펙이 변경되고, API를 사용하던 사용자들은 모두 장애를 겪게 될 것이다. 물론 @JsonProperty를 이용해 반환되는 값의 이름을 변경할 수 있지만, 이는 결국 Entity를 무겁게 만들어 근본적인 해결책이 될 수 없다.
스펙이 변경되어 테이블에 컬럼이 추가되는 경우도 마찬가지이다. 테이블에 새로운 컬럼이 추가되면 엔티티에 새로운 변수가 추가될 것이고, 별도로 처리를 하지 않는 이상 API 스펙이 변경되는 것이다.

이러한 상황을 위해 DTO를 이용해 분리하여 독립성을 높임으로써 변경이 전파되는 것을 방지해야 한다. 만약 우리가 응답을 위한 DTO 클래스를 활용하고 있으면, Entity 클래스의 변수가 변경되어도 API 스펙이 변경되지 않으므로 안정성을 확보할 수 있다.

 

4. API 스펙의 파악 용이

DTO를 사용함으로써 얻는 또 다른 장점은 DTO를 통해 API 스펙을 어느정도 파악할 수 있다는 점이다. API 문서의 요약본을 작성하는 것과 유사한 효과를 얻을 수 있으며, 특히 요즘같은 MSA 아키텍처로 개발을 많이 하는 상황에서 다른 사람이 작성한 API 호출 코드를 파악할 때 요청/응답을 손쉽게 파악할 수 있어 용이하다.

 

출처

https://mangkyu.tistory.com/192

 

[Spring] 엔티티(Entity) 또는 도메인 객체(Domain Object)와 DTO를 분리해야 하는 이유

개발을 하다 보면 API의 요청이나 응답을 처리할 때 또는 다른 계청으로 넘기는 파라미터가 너무 많은 시점에 별도의 DTO를 생성해야 하나 고민을 하는 시점이 생깁니다. 개인적으로는 간단한 애

mangkyu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !