네로개발일기

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

'전체 글'에 해당되는 글 194건


반응형

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

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

반응형

필요한 데이터를 저장하기 위해 Map<String, Object>를 사용하는 경우가 있다.

 

Map이 아닌 DTO 클래스를 사용해야 하는 이유

[Map을 사용할 때의 단점]

1. 컴파일 에러를 유발할 수 없음.

2. String 텍스트를 Key로 사용함.

3. 가독성이 떨어짐.

4. 타입캐스팅 비용이 발생함.

5. 불변성을 확보할 수 없음.

 

1. 컴파일 에러를 유발할 수 없음

Map의 Value는 Object 타입이다. 그리고 Object 클래스는 최상위 클래스이기 때문에 어떠한 데이터도 넣을 수 있다. Object를 사용할 때의 문제는 어떠한 데이터도 받아들일 수 있기 때문에 타입체크를 할 수 없다는 것이다. 잘못된 타입을 넣어줘도 컴파일 에러를 유발하지 않는다.

 

2. String 텍스트를 Key로 사용함

단순 String을 사용하는 것은 문제가 발생할 여지가 항상 열려있다는 것이다. 굳이 문제의 가능성을 열어두고 이로 인해 불필요한 시간 낭비의 여지를 줄 필요가 없다. DTO를 이용하면 이러한 문제를 해결할 수 있다.

 

3. 가독성이 떨어짐

Map을 사용하는 구조는 가독성이 떨어진다. 어떠한 데이터를 가지고 있는지를 확인할 때는 Map보다 DTO를 확인하는 것이 직관적이고 좋다. 만약 Map<String, Object>를 본다면, 우리가 받는 Key 값은 무엇이고, Value 값은 무엇이며 어떠한 타입인지를 파악하기가 쉽지않다. 만약 Map안에 또 다른 Map이 들어있다면 이러한 문제는 더욱 심각해진다. 결국 Map으로 작성된 코드를 이해하기 위해서는 불필요한 코드 리딩 시간이 필요하고 생산성이 떨어지게 된다.

 

4. 타입캐스팅 비용이 발생함

Map에 있는 데이터를 꺼내서 사용하기 위해서는 반드시 타입 캐스팅을 해야한다.

String name = (String) map.get("name");

이러한 타입캐스팅은 당연히 컴퓨팅 비용을 필요로 한다. 만약 꺼내야 하는 데이터가 많다면 이러한 비용은 더욱 증가하게 된다. 불필요한 타입 캐스팅 비용을 줄이기 위해서도 DTO를 사용하는 것이 좋다.

 

5. 불변성을 확보할 수 없음

Map을 사용하면 해당 데이터의 불변성을 확보할 수 없다. 만약 누군가가 실수로 put 코드를 추가하였다면 기존의 데이터는 없어지고 잘못된 데이터로 덮어 씌워진다.

Map<String, Object> map = new HashMap<>();
map.put("name", "jiyoon");

// 불변성을 확보할 수 없고 값이 변경될 수 있음.
map.put("name", "nero");

Map을 사용하면 불변성을 확보할 수 없으니 DTO를 사용하는 것이 좋다.

 

 

 

Map을 사용하면 위와 같은 단점들을 안게 된다. 이러한 이유로 DTO(Data Transfer Object)라는 데이터 전달 클래스를 사용하는 것이 좋다. DTO를 사용하면 추가적으로 정적 팩토리 메소드를 구현하여 많은 이점을 얻을 수도 있고, 빌더 패턴도 적용할 수 있어 상당히 유용하다. 실제로 업무를 하다보면 이러한 단점을 더욱 잘 체감할 수 있다.

물론 개인적으로 매우 제한적인 경우에 Map을 사용하기도 하는데, 현재도 그렇고 미래에도 절대적으로 단일 Key값을 갖는 케이스라면 컨트롤러에서 Collections.singletonMap로 응답을 반환하기도 한다. 그 외에도 Map을 사용해서 위에서 얘기한 단점들이 부각되지 않거나 유지보수성을 떨어뜨리지 않는다면 Map을 사용해서 오히려 좋아지는 케이스도 있다. 그렇기 때문에 상황을 고려해보았을 때, Map을 사용하여도 위에 적힌 단점들이 부각되지 않거나 오히려 이점을 얻을 수 있는 경우에는 Map을 사용해도 괜찮다. 하지만 위에서 얘기했던 단점들이 드러나는 상황이라면 Map보다는 DTO를 이용하는 것을 권장한다.



 출처 

https://mangkyu.tistory.com/164

 

[Java] Map보다 DTO 클래스를 사용해야 하는 이유

필요한 데이터를 저장하기 위해 Map 를 사용하는 개발자들이 있습니다. 하지만 Map을 사용하면 너무 많은 단점들을 안게 되는 것 같아서, 왜 Map이 아닌 DTO 클래스를 사용해야 하는지에 대해 정리

mangkyu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

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