네로개발일기

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

'web/Spring'에 해당되는 글 67건


반응형

@ModelAttribute

1. Method Level

@ModelAttribute
public void addAttributes(Model model) {
    model.addAttribute("msg", "Welcome to the Netherlands!");
}

하나 이상의 속성을 Model에 추가하고 싶을 때 메서드 레벨에서 @ModelAttribute를 추가해준다. 일반적으로 Spring MVC는 요청 핸들러 메서드를 호출하기 전에 항상 해당 메서드를 먼저 호출한다. 즉, @RequestMapping 어노테이션이 달린 컨트롤러 메서드가 호출되기 전에 @ModelAttribute 어노테이션이 달린 메서드가 호출된다. 시퀀스의 논리는 컨트롤러 메서드 내에서 처리가 시작되기 전에 모델 개체를 만들어야한다는 것이다.

 

2. Method Argument Level

@ModelAttribute는 여러 곳에 있는 단순 데이터 타입을 복합 타입 객체로 받아오거나 해당 객체를 새로 만들 때 사용할 수 있다.

여러 곳은 URI path, 요청 매개변수, 세션 등을 의미한다.

복합 객체에 데이터 바인딩을 하는 방법은 @RequestParameter로만 전달되어야하는 것은 아니다.

유연하게 여러 데이터를 하나의 복합 타입 객체로 받아올 수 있다.

 

 

만약 값을 바인딩할 수 없는 경우라면 BindException이 발생하고 400 에러가 발생한다.

만약 바인딩 에러를 직접 다루고 싶은 경우라면 @ModelAttribute가 붙은 메서드에 BindingResult 파라미터를 추가하면 된다.

@Controller
@RequestMapping("/event")
public class SampleController {
    
    @PostMapping("/names/{name}")
    @ResponseBody
    public Event getEvent(@ModelAttribute Event event, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            bindingResult.getAllErrors().forEach( c -> {
                System.out.println(c.toString());
            });
        }
        
        return event;
    }
}

 

바인딩 이후 검증 작업을 추가로 진행하고 싶다면 @Valid 또는 @Validated 어노테이션을 사용할 수 있다.

// Event.java
@Getter
@Setter
public class Event {

    private Long id;
    private String name;
    
    @Min(0)
    private Integer limit;
}

// SampleController.java
@Controller
@RequestMapping("/event")
public class SampleController {
    
    @PostMapping("/names/{name}")
    @ResponseBody
    public Event getEvent(@Valid @ModelAttribute Event event, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            bindingResult.getAllErrors().forEach( c -> {
                System.out.println(c.toString());
            });
        }
        
        return event;
    }
}

@Validated

스프링 MVC 핸들러 메서드 아규먼트에 사용할 수 있으며, Validation group 이라는 힌트를 사용할 수 있다.

@Valid 어노테이션에는 그룹을 지정할 방법이 없지만 @Validated는 스프링이 제공하는 어노테이션으로 그룹 클래스를 설정할 수 있다.

 

 참고 

https://www.baeldung.com/spring-mvc-and-the-modelattribute-annotation

https://minkukjo.github.io/framework/2020/04/13/Spring-85/

 

Spring MVC 핸들러 메소드 @ModelAttribute

Spring Web MVC

minkukjo.github.io

 

728x90
반응형
blog image

Written by ner.o

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

반응형

Lists in Thymelead Example

예제를 위해 Book.java 을 생성한다.

@Getter
@Setter
public class Book {

    private Long id;
    private String title;
    private String author;
}

 

Displaying List Elements

BookController.java

@GetMapping("/all")
public String showAll(Model model) {
    model.addAttribute("books", bookService.findAll());
    return "books/allBooks";
}

books/allBooks.html

<table>
  <thead>
    <tr>
      <td>Title</td>
      <td>Author</td>
    </tr>
  </thead>
    <tr th:if="${books.empty}">
      <td colspan="2">No Books Avaliable</td>
    </tr>
    <tr th:each="book : ${books}">
      <td th:text="${book.title}">Title</td>
      <td th:text="${book.author}">Author</td>
    </tr>
  <tbody>
  </tbody>
</table>

th:each 속성을 사용해서 list의 iterate 문을 사용할 수 있다.

Binding a List Using Selection Expression

@AllArgsConstructor
@Getter
@Setter
public class BooksCreationDto {
    
    private List<Book> books;
    
    public void addBook(Book book) {
        this.books.add(book);
    }
}

controller에서 List 객체를 전송하기 위해선 List 객체를 그대로 사용할 수 없다. 위와 같이 wrapper 객체를 만들어주어야 한다.

 

@GetMapping("/create")
public String showCreateForm(Model model) {
    BooksCreationDto booksForm = new BooksCreationDto();

    for (int i = 1; i <= 3; i++) {
        booksForm.addBook(new Book());
    }

    model.addAttribute("form", booksForm);
    return "books/createBooksForm";
}

Model 속성에 3개의 빈 Book 객체를 생성하여 추가하였다.

 

<form action="#" th:action="@{/books/save}" th:object="${form}"method="post">
    <fieldset>
        <input type="submit" id="submitButton" th:value="Save">
        <input type="reset" id="resetButton" name="reset" th:value="Reset"/>
        <table>
            <thead>
                <tr>
                    <th> Title</th>
                    <th> Author</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="book, itemStat : *{books}">
                    <td><input th:field="*{books[__${itemStat.index}__].title}" /></td>
                    <td><input th:field="*{books[__${itemStat.index}__].author}" /></td>
                </tr>
            </tbody>
        </table>
    </fieldset>
</form>

Thymeleaf를 실행하면 3개의 빈 Book 객체가 있을 것이다.

th:object="${form}"

form에서 submit을 할 때, form의 데이터가 th:object에 설정해준 객체로 받아진다.

<tr th:each="book, itemStat : *{books}">

각각 필드들을 매핑을 해주는 역할을 한다. 설정해 준 값으로, th:object에 설정해 준 객체의 내부와 매칭해준다.

@PostMapping("/save")
public String saveBooks(@ModelAttribute BooksCreationDto form, Model model) {
    bookService.saveAll(form.getBooks());

    model.addAttribute("books", bookService.findAll());
    return "redirect:/books/all";
}

@ModelAttribute 어노테이션을 활용하여 객체를 가져올 수 있다.

Book list를 저장하고 list 화면으로 redirect 해주면 다음과 같은 화면을 얻을 수 있다.

Binding a List Using Variable Expression

@GetMapping("/edit")
public String showEditForm(Model model) {
    List<Book> books = new ArrayList<>();
    bookService.findAll().iterator().forEachRemaining(books::add);

    model.addAttribute("form", new BooksCreationDto(books));
    return "books/editBooksForm";
}
<tr th:each="book, itemStat : ${form.books}">
    <td>
        <input hidden th:name="|books[${itemStat.index}].id|" th:value="${book.getId()}"/>
    </td>
    <td>
        <input th:name="|books[${itemStat.index}].title|" th:value="${book.getTitle()}"/>
    </td>
    <td>
        <input th:name="|books[${itemStat.index}].author|" th:value="${book.getAuthor()}"/>
    </td>
</tr>

적절하게 data를 submit하기 위해선 name과 value값을 지정해주어야 한다. edit 해주기 위해선 books.id도 전달해주어야 하는데, hidden 속성을 통해 보이지 않게 전달하고 있다.

 

출처

https://www.baeldung.com/thymeleaf-list

 

728x90
반응형
blog image

Written by ner.o

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

반응형

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

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