LandvibeDev / 2024-spring5-programming-introduction

스프링5 프로그래밍 입문 레포지토리입니다. (2024 썸머코딩 서버)
0 stars 0 forks source link

[5주차] 수빈, 지현, 예원 #6

Open tmdcheol opened 1 month ago

tmdcheol commented 1 month ago

스프링 프로그래밍 입문 5 남은 부분 정독

InSooBeen commented 1 month ago

15장 간단한 웹 어플리케이션의 구조

간단한 웹 어플리케이션의 구성 요소


간단한 웹 어플리케이션을 개발할 때 사용하는 전형적인 구조

프론트 서블릿

웹 브라우저의 모든 요청을 받는 창구이다. 요청을 분석해서 알맞은 컨트롤러에 전달한다. 스프링 MVC에서는 DispatcherServlet이 프론트 서블릿의 역할을 수행한다.

컨트롤러

실제 웹 브라우저의 요청을 처리하는 역할이다. 클라이언트가 요구한 기능 수행, 응답 결과를 생성하는데 필요한 모델 생성, 응답 결과를 생성할 뷰 선택을 담당한다.

컨트롤러는 어플리케이션이 제공하는 기능과 사용자 요청을 연결하는 매개체로, 기능 제공을 위한 로직을 직접 수행하지는 않는다. 대신, 해당 로직을 제공하는 서비스에 그 처리를 위임한다.

@PostMapping
    public String submit(
            @ModelAttribute("command") ChangePwdCommand pwdCmd,
            Errors errors,
            HttpSession session) {
        new ChangePwdCommandValidator().validate(pwdCmd, errors);
        if (errors.hasErrors()) {
            return "edit/changePwdForm";
        }
        AuthInfo authInfo = (AuthInfo) session.getAttribute("authInfo");
        try {
            //컨트롤러는 로직 실행을 서비스에 위임한다.
            changePasswordService.changePassword(
                    authInfo.getEmail(),
                    pwdCmd.getCurrentPassword(),
                    pwdCmd.getNewPassword());
            return "edit/changedPwd";
        } catch (WrongIdPasswordException e) {
            errors.rejectValue("currentPassword", "notMatching");
            return "edit/changePwdForm";
        }
    }

위의 코드는 ChangePasswordController의 일부분이다. ChangePasswordController에서는 직접 비밀번호 변경 로직을 실행하지 않고, ChangePasswordService에 비밀번호 변경 처리를 위임한 것을 알 수 있다.

서비스

비지니스 로직을 처리하고, 컨트롤러와 데이터 액세스 계층 사이에서 중간 역할을 수행한다. 서비스는 DB연동이 필요하면 DAO를 사용한다.

DAO (Data Access Object)

데이터베이스와 웹 어플리케이션 사이에서 데이터의 상호작용에 사용되는 객체이다.

서비스의 구현


서비스 구현 예시 : 비밀번호 변경 기능

비밀번호 변경 기능은 아래의 로직을 서비스에서 수행한다.

서비스에서 실행하는 로직은 한 번의 과정으로 끝나지 않고, 몇 단계에 걸쳐 진행되는 경우가 많다.

따라서, 모든 과정이 성공적으로 진행되었을 때만 완료 처리를 하고, 중간 과정에서 실패하면 이전까지 했던 것을 모두 취소해야 한다.

⇒ 서비스 메서드는 트랜잭션 범위에서 실행해야 한다.

스프링에서는 @Transactional 을 이용하여 트랜잭션 범위에서 서비스 기능을 수행할 수 있다.

@Transactional
    public void changePassword(String email, String oldPwd, String newPwd) {
        Member member = memberDao.selectByEmail(email);
        if (member == null)
            throw new MemberNotFoundException();

        member.changePassword(oldPwd, newPwd);

        memberDao.update(member);
    }

@Transactional을 사용했으므로, 비밀번호 변경 서비스는 중간 과정에서 실패하면 rollback되고, 모든 과정이 성공된 경우에만 commit 된다.

서비스 기능을 제공하는 메서드에서 필요한 데이터를 전달받는 방법은 다음과 같다.

public void changePassword(String email, String oldPwd, String newPwd)
public void regist(RegisterRequest req)
@PostMapping
    public String submit(
            @ModelAttribute("command") ChangePwdCommand pwdCmd,
            Errors errors,
            HttpSession session) {
        ... 생략
            changePasswordService.changePassword(
                    authInfo.getEmail(),
                    pwdCmd.getCurrentPassword(),
                    pwdCmd.getNewPassword());
        ... 생략
    }

별도의 타입을 만들어 스프링 MVC의 커맨드 객체로 사용하는 방법이 편하다.

커맨드 클래스를 직접 작성하면 스프링 MVC가 제공하는 폼 값 바인딩과 검증, 스프링 폼 태그와의 연동 기능을 사용할 수 있다.

서비스 메서드는 기능을 실행한 후 결과를 알려주어야 하며, 결과를 알려주는 방식은 크게 2가지가 있다.

public class AuthService {
    ... 생략

    public AuthInfo authenticate(String email, String password) {
        Member member = memberDao.selectByEmail(email);
        if (member == null) {
            throw new WrongIdPasswordException();
        }
        if (!member.matchPassword(password)) {
            throw new WrongIdPasswordException();
        }
        return new AuthInfo(member.getId(),
                member.getEmail(),
                member.getName());
    }

}

authenticate 메서드

⇒ 인증에 성공할 경우 인증 정보를 담고있는 AuthInfo 객체를 리턴해서 정상적으로 실행되었음을 알려준다.

⇒ 익셉션의 발생을 통해 인증이 실패되었음을 알려준다.

컨트롤러에서의 DAO 접근


기본적으로 서비스에서 DAO에 접근하는 경우가 많다.

만약 서비스 메서드에서 어떤 로직도 수행하지 않고 단순히 DAO의 메서드만 호출하고 끝난다면?

public class MemberService{
...
    public Member getMember(Long id){
        return memberDao.selectById(id);
    }
}
@RequestMapping("/member/detail/{id}")
public String detail(@PathVariable("id") Long id, Model model){
    //사실상 DAO를 직접 호출하는 것과 동일
    Member member = memberService.getMember(id);
    if(member==null){
        return "member/notFound";
    }
    model.addAttribute("member", member);
    return "member/memberDetail"
}

memberService.getMember(id) 는 사실상 memberDao.selectById() 메서드를 실행하는 것과 동일하다.

⇒ 컨트롤러를 사용한다는 압박감에서 벗어나 DAO에 직접 접근해도 큰 틀에서는 웹 어플리케이션의 계층 구조는 유지되는 것으로 본다.

패키지 구성


웹 어플리케이션에 사용된 구성요소 패키지를 구분하기 위한 영역은 웹 요청을 처리하기 위한 영역과 기능을 제공하기 위한 영역으로 나눌 수 있다.

커맨드 객체의 값을 검증하기 위한 Validator는 관점에 따라 두 영역 중 선택하여 위치시킨다.

웹 어플리케이션이 복잡해지면?

컨트롤러-서비스-DAO 구조는 간단한 웹 어플리케이션을 개발할 때는 괜찮지만, 기능이 많아지고 로직이 추가되면 구조적인 부분의 코드도 함께 복잡해진다.
⇒ 특정 기능을 분석하는 시간이 증가, 중요한 로직을 구현한 코드가 흩어짐, 중복된 퀴리 및 로직 코드 등의 문제가 발생한다.

도메인 주도 설계의 적용

위에서 발생한 문제를 해결하기 위한 방법 중 하나이다. UI-서비스-도메인-인프라 구조로 어플리케이션을 구성하며, UI와 인프라는 각각 컨트롤러와 DAO 영 역에 대응한다. 다만, 도메인 모델 및 업무 로직이 서비스 영역이 아닌 도메인 영역에 위치한다는 차이점이 있다. 도메인 영역은 정해진 패턴에 따라 모델을 구현하므로, 업무가 복잡해져도 일정 수준의 복잡도를 유 지할 수 있다.

16장 JSON 응답과 요청 처리

JSON 개요


JSON(JavaScript Object Notation)은 간단한 형식을 갖는 문자열로, 데이터 교환에 주로 사용된다.

JSON 형식으로 표현한 데이터

{
    "name":"유관순",
    "age":"17",
    "related":["남동순","류예도"],
    ...
}

중괄호를 사용해 객체를 표현하며, 객체는 콜론으로 구분된 (이름, 값) 쌍을 갖는다.

Jackson 의존 설정


Jackson은 자바 객체와 JSON 형식 문자열 간의 변환을 처리하는 라이브러리이다. 스프링 MVC에서 Jackson 라이브러리를 이용해 자바 객체를 JSON으로 변환하려면 클래스에 Jackson 라이브러리를 추가해야 한다.

build.gradle에는 다음 코드를 추가하면 된다.

dependencies {
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' // 적절한 버전
}

Jackson은 프로퍼티의 이름과 값을 JSON 객체의 (이름, 값) 쌍으로 변환한다.

프로퍼티 타입이 배열이나 List인 경우 JSON 배열로 변환된다.

@RestController로 JSON 형식 응답


스프링 MVC에서는 @Controller 대신 @RestController를 사용하면 JSON 형식으로 데이터를 응답할 수 있다.

@RestController를 이용하면 스프링 MVC는 요청 매핑 애노테이션을 붙인 메서드가 리턴한 객체를 알맞은 형식으로 변환해서 응답 데이터로 전송하는데, 이 과정에서 클래스 Path에 Jackson이 존재하면 JSON 형식으로 변환해서 응답한다.

@JsonIgnore를 이용한 제외 처리


암호와 같이 민감한 데이터의 경우 응답 결과에 포함시키지 않아야 한다.
Jackson이 제공하는 @JsonIgnore을 이용하면 제외 처리를 할 수 있다.

import com.fasterxml.jackson.annotation.JsonIgnore;

public class Member{
    private Long id;
    private String email;
    @JsonIgnore 
    private String password;
    private String name;
    private LocalDateTime localDateTime;
}

제외하고자 하는 대상에 @JsonIgnore를 붙이면 된다.

@JsonFormat을 이용한 날짜 형식 변환 처리


날짜나 시간은 배열이나 숫자보다 “0000-00-00 00:00:00”과 같이 특정 형식을 갖는 문자열로 표현하는 것이 선호된다. Jackson에서는 @JsonFormat을 이용하면 날짜 및 시간을 특정한 형식으로 지정할 수 있다.

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;

public class Member{

    private Long id;
    private String email;
    private String name;
    @JsonFormat(shape=Shape.STRING) //ISO-8601 형식으로 변환
    private LocalDateTime registerDateTime;
}

위의 코드에서는 ISO-8601 형식을 이용하여 날짜와 시간을 표현한다.

ISO-8601 형식이 아닌, 원하는 형식으로 변환해서 출력하고 싶으면 pattern 속성을 사용하면 된다.

import com.fasterxml.jackson.annotation.JsonFormat;

public class Member{

    private Long id;
    private String email;
    private String name;
    @JsonFormat(pattern="yyyyMMddHHmmss") 
    private LocalDateTime registerDateTime;
}

기본 적용 설정을 이용한 날짜 형식 변환 처리


날짜 형식을 변환할 모든 대상에 @JsonFormat을 붙여야 한다면 번거로워지므로 날짜 타입에 해당하는 모든 대상에 동일한 변환 규칙을 적용하는 방법이 있다.

@JsonFormat을 사용하지 않고 Jackson의 변환 규칙을 모든 날짜 타입에 적용하려면, 스프링 MVC 설정을 변경해야 한다.

스프링 MVC는 자바 객체를 HTTP 응답으로 변환할 때 HttpMessageConverter를 사용하므로, JSON으로 변환할 때 사용하는 HttpmessageConverter를 새롭게 등록해서 날짜 형식을 원하는 형식으로 변환하도록 설정하면 모든 날짜 형식에 동일한 변환 규칙을 적용할 수 있다.


@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
...생략
@Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    //Jackson2ObjectMapperBuilder = ObjectMapper를 쉽게 생성하도록 스프링이 제공하는 클래스
        ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
                .json() 
                //Json이 유닉스 타임 스탬프로 날짜형식을 출력하는 것을 비활성화 => ISO-8601 형식
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .build();
                //새로 생성한 objectMapper를 사용하는 객체를 converters의 첫번째 항목으로 등록
        converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
    }
}

extendMessageConverters 메서드는 WebMvcConfigurer 인터페이스에 정의된 메서드로 HttpMessageConverter를 추가로 설정할 때 사용한다.

@EnableWebMvc을 사용하면 스프링 MVC는 여러 형식으로 변환할 수 있는 HttpMessageConverter를 미리 등록하며, extendMessageConverter 메서드는 등록된 HttpMessageConverter 목록을 파라미터로 받는다.

HttpMessageConverter에는 Jackson을 이용하는 것도 포함되어 있어, 새로 생성한 HttpMessageConverter는 목록의 제일 앞에 위치시켜야 한다.

⇒ converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));

@RequestBody로 JSON 요청 처리


이번에는 JSON 형식의 요청 데이터를 자바 객체로 변환하는 기능에 대해 살펴보자.

POST나 PUT 방식을 이용하면 JSON 형식의 데이터를 요청 데이터로 전송할 수 있으며, 커맨드 객체에 @RequestBody를 붙이면 데이터를 전달받을 수 있다.

@RestController
public class RestMemberController {
    private MemberDao memberDao;
    private MemberRegisterService registerService;
    ...생략

    @PostMapping("/api/members")
    public void newMember(
            @RequestBody @Valid RegisterRequest regReq, 
            HttpServletResponse response) throws IOException {
        try {
            Long newMemberId = registerService.regist(regReq);
            response.setHeader("Location", "/api/members/" + newMemberId);
            response.setStatus(HttpServletResponse.SC_CREATED);
        } catch (DuplicateMemberException dupEx) {
            return sendError(HttpServletResponse.SC_CONFLICT);
        }
    }
}

주의사항

스프링 MVC가 JSON 형식으로 전송된 데이터를 올바르게 처리하려면 요청 컨텐츠 타입이 application/json이어야 하는데, 보통 POST 방식의 폼 데이터는 컨텐츠 타입이 application/x-www-form-urlencoded이다. 따라서 별도의 프로그램을 필요된다.

ex) 크롬 - Advanced REST client, Postman

요청 객체 검증하기


JSON 형식으로 전송한 데이터를 변환한 객체는 @Vaild나 별도의 Validator를 이용해서 검증할 수 있다.

Validator를 사용하면 직접 상태코드를 처리해야 한다.

@PostMapping("/api/members")
public void newMember(
@RequestBody RegisterRequest regReg, Erros erros,
HttpServletResponse response) throws IOException{
    try{
    //직접 생태코드 처리
        new RegisterRequestValidator().validate(regReg, erros);
        if(errors.hasErrors()){
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        ...
    }catch(DuplicationMemberException dupEx){
        response.sendError(HttpServletResponse.SC_CONFLICT);
    }
}

@ResponseEntity로 객체 리턴하고 응답 코드 지정하기


그동안의 예제에서는 상태코드를 지정하기 위해 HttpServletResponse의 setStatus 메서드와 sendError 메서드를 이용했다.

HttpServletResponse를 이용하여 404 응답을 하면 JSON 형식이 아닌 HTML을 응답결과로 제공하는데, API를 호출하는 프로그램 입장에서는 JSON과 HTML의 응답을 모두 처리하는 것이 부담스럽다.

⇒ 처리에 실패한 경우에는 HTML 응답 데이터 대신 JSON 형식의 데이터를 전송해야 API 호출 프로그램이 일관된 방법으로 응답을 처리할 수 있다.

@ResponseEntity를 사용하면 정상인 경우와 비정상인 경우 모두 JSON 응답을 전송할 수 있다.


@RestController
public class RestMemberController {
    private MemberDao memberDao;
    private MemberRegisterService registerService;
    ...생략

    @GetMapping("/api/members/{id}")
    public ResponseEntity<Object> member(@PathVariable Long id) {
        Member member = memberDao.selectById(id);
        if (member == null) {
        //body를 ErrorResponse로 설정
            return ResponseEntity
                    .status(HttpStatus.NOT_FOUND)
                    .body(new ErrorResponse("no member"));
        }
        //body를 member로 설정
        return ResponseEntity.status(HttpStatus.OK).body(member);
    }
}

스프링 MVC는 리턴 타입이 ResponseEntiry이면 ResponseEntity의 body로 지정한 객체를 사용해서 변환을 처리한다. 위의 예제에서는 ResponseEntity의 body를 지정한 부분에서 ErrorResponse, member 객체를 리턴하므로 각각 해당 객체들을 JSON으로 변환한다.

따라서, status와 body를 이용해 상태코드와 JSON으로 변환할 객체를 지정하면 ResponseEntity을 생성해 이용할 수 있다.

@ExceptionHandler 적용 메서드에서 ResponseEntity로 응답하기


한 메서드에서 정상 응답과 에러 응답을 ResponseEntity로 생성하면 코드가 중복될 수도 있다.

@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable Long id){
    Member member = memberDao.selectById(id);
    if(member==null){
        //member가 존재하지 않을 때 JSON 응답을 제공하기 위한 ResponseEntity
        return ResponseEntity
                    .status(HttpStatus.NOT_FOUND)
                    .body(new ErrorResponse("no member");
    }
    return ResponseEntity.ok(member);
}

위의 코드에서는 member가 존재하지 않을 때 HTML 응답 대신 JSON 응답을 제공하기 위한 ResponseEntity를 사용한다.

그런데 회원이 존재하지 않을 때 404 상태 코드를 응답해야 하는 기능이 많다면 에러 응답을 위해 ResponseEntity를 생성하는 코드가 여러 곳에 중복된다.

⇒@ExceptionHandler를 적용한 메서드에서 에러 응답을 처리하도록 구현하면 중복을 없앨 수 있다!

@GetMapping("/api/members/{id}")
public Member member(@PathVariable Long id){
Member member = memberDao.selectById(id);
if(member==null){
    throw new MemberNotFoundException();
}
return member;
}
//MemberNotFoundException에 대한 응답 처리
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData(){
    return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("no member"));
}

위의 코드에서는 member 메서드가 회원 데이터가 존재하면 Member 객체를 리턴하므로 JSON으로 변환된 결과를 응답한다.

회원 데이터가 존재하지 않으면 MemberNotFoundException을 발생하는데, 이 Exception이 발생하면 @ExceptionHandler를 사용한 handleNoData 메서드가 에러를 처리하므로 404 상태 코드와 ErrorResponse 객체를 몸체로 갖는 ResponseEntity를 리턴한다.

@RestControllerAdvice를 이용하여 에러 처리 코드를 별도의 클래스로 분리하기

@RestControllerAdvice("controller")
public class ApiExceptionAdvice{

    @ExceptionHandler(MemberNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNoData(){
        return ResponseEntity
                    .status(HttpStatus.NOT_FOUND)
                    .body(new ErrorResponse("no member"));
    }
}

⇒ 에러 처리 코드가 한 곳으로 모여 효과적으로 에러 응답을 관리할 수 있다.

@Valid 에러 결과를 JSON으로 응답하기


@Valid를 붙인 커맨드 객체가 값 검증에 실패하면 400 상태코드를 응답하는데, HttpServletResponse 와 마찬가지로 HTML을 응답 결과로 전송한다.

HTML 응답 데이터 대신 JSON 형식의 응답 데이터를 제공하고 싶다면 Errors 타입 파라미터를 추가해서 직접 응답 에러를 생성하면 된다.

@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
    @RequestBody @Valid RegisterRequest regReq,
    Errors errors){
if(errors.hasErrors()){
    String errorCodes = errors.getAllErrors()
        .stream()
        .map(error->error.getCodes()[0])
        .collect(Collectors.joining(","));
    return ResponseEntity
        .status(HttpStatus.BAD_REQUEST)
        .body(new ErrorResponse("errorCodes = " + errorCodes));
    }
    ...생략
}

위의 코드는 hasErrors 메서드를 이용하여 검증 에러가 존재하는지 확인한다. 검증에러가 존재하면 getAllErrors() 메서드로 모든 에러 정보를 구하고 각 에러의 코드 값을 연결한 문자열을 생성해서 errorCodes 변수에 할당한다.

@RequestBody를 사용하면 @Valid를 붙인 객체 검증에 실패했을 때 Errors 타입 파라미터가 존재하지 않으면 MethodArgumentNotValidException이 발생한다.

⇒ @ExceptionHandler를 통해 검증에 실패했을 때의 에러 응답을 생성하면 된다.

17장 프로필과 프로퍼티 파일

프로필


개발을 진행하는 동안에는 실제 서비스를 목적으로 운영중인 DB를 이용할 수 없으며, 실제 서비스 환경에서는 웹 서버와 DB 서버가 서로 다른 장비에 설치된 경우가 많다. 개발 환경에서 사용한 DB 계정과 실제 서비스 환경에서 사용할 DB 계정이 다른 경우도 흔하다.

개발을 완료한 어플리케이션을 실제 서버에 배포하려면 실제 서비스 환경에 맞는 JDBC 연결 정보를 사용해야 한다.

실제 서비스 배포 전에 설정 정보를 변경하고 배포하면 안되는가?

실수의 여지가 많은 방식이다.

실수를 방지하려면?

처음부터 개발 목적과 실제 서비스 목적의 설정을 구분해서 작성하면 된다. 이를 위한 스프링 기능을

프로필이라고 한다.

프로필 설정

설정 집합에 프로필을 지정할 수 있으며 스프링 컨테이너는 설정 집합 중 지정한 이름을 사용하는 프로필을 선택하고 해당 프로필에 속한 설정을 이용해서 컨테이너를 초기화할 수 있다.

ex) 개발 환경을 위한 DataSource 설정을 dev 프로필, 실제 서비스 환경을 위한 DataSource 설정을 real 프로필로 지정했을 때

dev 프로필을 사용해 스프링 컨테이너를 초기화하면 dev 프로필에 정의된 빈을 사용한다.

프로필 설정 방법


설정방법 1

@Configuration을 이용한 설정에서 @Profile을 이용해 프로필을 지정할 수 있다.

@Configuration
@Profile("dev")
public class DsDevConfig{

    //설정 클래스 구현

}
@Configuration
@profile("real")
public class DsRealConfig{

    //설정 클래스 구현

}

스프링 컨테이너를 초기화 할때 dev와 real 중 하나를 지정하면 컨테이너는 지정된 설정 클래스의 빈만 이용하여 초기화한다.

특정 프로필을 선택하려면?

스프링 컨테이너를 초기화 하기 전에 setActiveProfiles 메서드를 사용해 프로필을 선택하면 된다.

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//프로필 지정
context.getEnvrionment().setActiveProfiles("dev");
//dev 프로필에 속한 설정 클래스인 DsDevConfig가 사용된다.
context.register(MemberConfig.class, DsDevConfig.class, DsRealConfig.class);
context.refresh();

프로필 사용 주의사항

설정 정보를 전달하기 전에 어떤 프로필을 사용할 지 지정해야한다.

프로필 지정 전에 설정 정보를 전달하면 설정을 읽어오는 과정에서 빈을 찾지 못해 Exception이 발생한다.

두개 이상의 프로필 활성화

context.getEnvironment().setAcitveProfiles("dev", "mysql");

활성화하고자 하는 프로필을 모두 파라미터로 전달하면 된다.

설정방법 2

spring.profiles.active 시스템 프로퍼티에 사용할 프로필로 값을 지정할 수 있다.

java -Dspring.profiles.active=dev main.Main

명령행에서 -D 옵션을 이용하거나 System.setProperty()를 이용해 지정할 수 있다. 위의 코드는 -D 옵션을 이용한 예시이다.

위와 같이 시스템 프로퍼티로 프로필을 설정하면 setActiveProfiles 메서드를 이용하지 않아도 dev 프로필이 활성화된다.

설정방법 3

OS의 spring.profiles.active 환경 변수에 값을 설정해도 된다.

프로필 우선순위

1위: setActiveProfiles

2위: 자바 시스템 프로퍼티

3위: OS 환경 변수

@Configuration을 이용한 프로필 설정


중첩 클래스를 이용하여 프로필 설정을 한 곳으로 모을 수 있다.

@Configuration
public class MemberConfigWithProfile{
    @Autowired
    private DataSource dataSource;

    @Bean
    public MemberDao memberDao(){
        return new MemberDao(dataSource);
    }

    @Configuration
    @Profile("dev")
    public static class DsDevConfig{
        //설정 클래스 구현
    }

    @Configuration
    @Profile("real")
    public static class DsRealConfig{
        //설정 클래스 구현
    }
}

단, @Configuration을 사용할 때, 중첩 클래스는 static 이어야 한다.

다수 프로필 지정


2개 이상의 프로필 이름 지정

@Configuration
@Profile("real, test")
public class DataSourceJndiConfig{
    ...생략
}

위의 코드를 작성하면 real 프로필과 test 프로필을 사용할 때 모두 DataSourceJndiConfig 설정을 사용한다.

느낌표를 사용한 프로필 지정

@Configuratijon
@Profile("!real")
public class DsDevConfig{
    //설정 클래스 구현
}

프로필 이름에 느낌표를 붙이면 해당 클래스가 활성화되지 않은 경우에 사용한다는 의미이다.

어플리케이션에서 프로필 설정하기


웹 어플리케이션의 프로필 설정

프로퍼티 파일을 이용한 프로퍼티 설정


스프링은 외부의 프로퍼티 파일을 이용해 스프링 빈을 설정하는 방법을 제공한다.

ex)application.properties 파일

db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost/spring5fs?chararcterEncoding=utf8
db.user=spring5
db.password=spring5

프로퍼티 파일의 값을 자바 설정에서 사용할 수 있기 때문에 설정의 일부를 외부 프로퍼티 파일을 이용해 변경할 수 있다.

@Configuration을 이용한 자바 설정에서의 프로프티 사용


자바 설정에서 프로퍼티 파일을 사용하기 위해 필요한 것


@Configuration
public class PropertyConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        configurer.setLocations(
                new ClassPathResource("db.properties"),
                new ClassPathResource("info.properties"));
        return configurer;
    }

}

PropertySourcesPlaceholderConfigurer의 setLocations 메서드는 프로퍼티 파일 목록을 인자로 전달받으므로 스프링의 Resource 타입을 이용해 파일 경로를 전달한다.

PropertySourcePlaceholderConfigurer 타입의 빈을 설정하는 메서드가 static인 이유?

특수한 목적의 빈이기 때문에 정적 메서드로 지정하지 않으면 원하는 대로 작동하지 않는다.

@Configuration
public class DsConfigWithProp {
    @Value("${db.driver}")
    private String driver;
    @Value("${db.url}")
    private String jdbcUrl;
    @Value("${db.user}")
    private String user;
    @Value("${db.password}")
    private String password;

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        DataSource ds = new DataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(jdbcUrl);
        ds.setUsername(user);
        ds.setPassword(password);
        ds.setInitialSize(2);
        ds.setMaxActive(10);
        ds.setTestWhileIdle(true);
        ds.setMinEvictableIdleTimeMillis(60000 * 3);
        ds.setTimeBetweenEvictionRunsMillis(10 * 1000);
        return ds;
    }

}

@Value이 ${구분자} 형식의 플레이스홀더를 값으로 갖고 있으면 플레이스홀더의 값을 일치하는 프로퍼티 값으로 치환한다.

⇒ 실제 빈을 생성하는 메서드는 @Value이 붙은 필드를 통해 해당 프로퍼티 값을 사용할 수 있다.

빈 클래스에서 사용하기


빈으로 사용할 클래스에도 @Value을 붙일 수 있다.

public class Info{

    @Value("${info.version}")
    private String version;

    public void printInfo(){
        System.out.println("version = " + version);
    }

    public void setVersion(String version){
        this.version = version;
    }
}

@Value을 필드에 붙이면 플레이스홀더에 해당하는 프로퍼티를 필드에 할당한다.

    @Value("${ifo.version}")
    public void setVersion(String version){
        this.version = version;
    }

세터 메서드에도 @Value을 적용할 수 있다.

Yewon2ee commented 1 month ago

Chapter 10: 스프링 MVC 프레임워크 동작 방식

1.스프링 MVC 핵심 구성 요소와 각 요소 간의 관계

image

동작 과정

  1. 요청 수신

    • 중앙에 있는 DispatcherServlet은 모든 연결을 담당
    • 웹 브라우저로부터 요청이 들어오면 DispatcherServlet은 그 요청을 처리하기 위한 컨트롤러 객체를 검색
    • 직접 검색하지는 않고 HandlerMapping이라는 빈 객체에 검색을 요청 (2번 과정)
  2. 컨트롤러 검색

    • HandlerMapping은 클라이언트 요청 경로를 이용해서 처리할 컨트롤러 빈 객체를 찾아 DispatcherServlet에 전달
    • 웹 요청 경로가 /hello면 등록된 컨트롤러 빈 중에서 /hello 요청 경로를 처리할 컨트롤러 리턴 (@WebServlet("/hello") 애노테이션이 붙은 것)
  3. 요청 처리

    • DispatcherServlet이 컨트롤러 객체를 받았다고 해서 바로 컨트롤러 객체의 메서드를 쓸 수 있는 건 아님
    • DispatcherServletHandlerAdapter의 빈에게 요청, 처리를 위임
    • HandlerAdapter가 컨트롤러의 알맞은 메서드를 호출해서 요청을 처리하고 그 결과를 ModelAndView라는 객체로 변환해서 DispatcherServlet에 리턴 (3~6번 과정)
  4. 뷰 선택

    • HandlerAdapter로부터 ModelAndView를 받은 DispatcherServlet은 결과를 보여줄 뷰를 찾기 위해 ViewResolver를 사용 (7번 과정)
  5. 뷰 객체 생성

    • ModelAndView는 컨트롤러가 리턴한 뷰 이름을 담고 있음
    • ViewResolver가 이 뷰 이름에 해당하는 뷰 객체를 찾거나 생성해서 리턴
    • 응답을 생성하기 위해 JSP를 사용하는 ViewResolver는 매번 새로운 뷰 객체를 생성해서 DispatcherServlet에 리턴
  6. 응답 생성

    • DispatcherServlet은 리턴 받은 뷰 객체에게 응답 결과 생성을 요청 (8번 과정)
    • JSP를 사용하는 경우 뷰 객체는 JSP를 실행함으로써 웹 브라우저에 전송할 응답 결과를 생성

1.1 컨트롤러와 핸들러

왜 컨트롤러를 찾아주는 객체는 'ControllerMapping'이 아니고 'HandlerMapping' 일까?

@Controller애노테이션을 붙인 클래스를 이용해 클라이언트 요청을 처리할 수도 있지만 원하면 다른 클래스 이용해서 처리할 수도 있다 예시: 스프링이 클라이언트의 요청을 처리하기 위해 제공하는 타입 중 HttpRequestHandler

-> 컨트롤러라는 특정용어 쓰는 대신 스프링MVC가 지원하는 다양한 요청처리객체를 표현함

HandlerAdapter를 거치는 이유

DispatcherServlet은 핸들러 객체의 실제 타입에 상관없이 실행 결과를 ModelAndView라는 타입으로 받을 수만 있으면 되는데 이때 핸들러의 실제 구현 타입에 따라 실행 결과를 ModelAndView 로 리턴해주는 객체도 있고 아닌 객체도 있다. -> 핸들러의 처리결과를 ModelAndView 로 변환해는 객체가 필요하다

+

핸들러의 객체 실제 타입마다 그에 맞는 HandlerMapping이랑 HandlerAdapter를 써야함. 맞는 HandlerMapping이랑 HandlerAdapter를 스프링 빈으로 등록해아함 -> 뒤에 설명 나온다

2.DispatcherServlet과 스프링 컨테이너

image

DispatcherServlet가 초기화 될때마다,설정 파일에 따라 새로운 WebApplicationContext를 생성한다. 웹 요청을 처리하기 위해 필요한 빈들을 포함하고 있어야하기 때문에 HandlerMapping, HandlerAdapter,Controller,ViewResolver 빈에 대한 정의가 있어야한다

메인 컨테이너와 서브 컨테이너의 차이점

메인 컨테이너(ApplicationContext)는 애플리케이션 시작 시 한 번만 생성 서브 컨테이너(WebApplicationContext)는 각 DispatcherServlet이 초기화될 때 생성

메인 컨테이너는 전체 애플리케이션에서 공통적으로 필요한 빈들을 관리 서브 컨테이너는 특정 DispatcherServlet에 대한 요청 처리를 담당하며, 웹 요청과 관련된 빈들을 관리

메인 컨테이너에는 데이터베이스, 보안, 트랜잭션 관리 등 애플리케이션 전반에서 필요한 빈들이 포함. 서브 컨테이너에는 요청을 처리하는 데 필요한 웹 계층의 빈들이 포함

3.@Controller를 위한 HandlerMapping과 HandlerAdapter

핸들러의 객체 실제 타입마다 그에 맞는 HandlerMapping이랑 HandlerAdapter를 써야함 해당하는 HandlerMapping이랑 HandlerAdapter를 스프링 빈으로 등록해아함 직접 작성은 복잡하므로 @EnableWebMvc 애노테이션 쓰면 매우 다양한 스프링 빈 설정을 추가해준다 이 때 빈으로 추가해주는 클래스 중에서 @controller타입의 핸들러를 처리하기 위한 클래스인 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter도 존재한다.

예시와 요청 처리 흐름

 @Controller
public class HelloController {

    @RequestMapping("/hello")
    public String sayHello(Model model) {
        model.addAttribute("message", "Hello, World!");
        return "helloView"; 
    }
}

요청 매핑: RequestMappingHandlerMapping이 URL /hello를 HelloController의 sayHello 메서드에 매핑

핸들러 호출: RequestMappingHandlerAdapter가 sayHello 메서드를 호출 Model 객체에 데이터("message": "Hello, World!")를 추가

뷰 반환: sayHello 메서드는 "helloView"라는 뷰 이름을 반환 RequestMappingHandlerAdapter는 이 뷰 이름과 모델 데이터를 포함하여 ModelAndView 객체를 생성 ViewResolver가 "helloView"를 실제 뷰 파일로 변환하여 사용자에게 응답

4.WebMvcConfigurer 인터페이스와 설정

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/view/");
        resolver.setSuffix(".jsp");
        registry.viewResolver(resolver);
    }
}

여기서 @EnableWebMvc 애노테이션을 사용하면 @Controller 애노테이션을 붙인 컨트롤러를 위한 설정을 생성한다 -> @Controller 애노테이션이 붙은 클래스들이 제대로 동작할 수 있게 필요한 설정들을 자동으로 해준다.

기본 설정만으로 부족한 부분은 @EnableWebMvc 애노테이션을 사용해 WebMvcConfigurer타입의 빈을 이용해 mvc 설정을 추가로 생성한다. 예시: MvcConfig 클래스는 WebMvcConfigurer 인터페이스를 구현하여 추가적인 설정을 제공한다:

  1. configureDefaultServletHandling 메서드: 기본 서블릿 처리를 활성화한다. 이로 인해 정적 리소스(예: 이미지, CSS 파일 등)가 올바르게 제공될 수 있다.

  2. configureViewResolvers 메서드: 뷰 리졸버를 설정하여 JSP 파일들을 /WEB-INF/view/ 폴더 아래에서 찾고 .jsp 확장자로 된 파일을 뷰로 사용할 수 있게 한다.

정리: @EnableWebMvc를 사용하면 스프링이 웹 애플리케이션을 위한 기본 설정을 해주고, WebMvcConfigurer를 이용해서 이 기본 설정을 너의 애플리케이션에 맞게 추가 또는 변경할 수 있다

5.JSP를 위한 ViewResolver

ViewResolver 설정:

@Bean
public ViewResolver viewResolver() {
    InternalResourceViewResolver vr = new InternalResourceViewResolver();
    vr.setPrefix("/WEB-INF/view/");
    vr.setSuffix(".jsp");
    return vr;
}

InternalResourceViewResolver는 뷰 이름에 접두사(prefix)와 접미사(suffix)를 추가하여 경로를 생성. 예: 뷰 이름이 "welcome"이면 "WEB-INF/view/welcome.jsp" 경로를 사용.

이 경로에 있는 JSP 파일을 InternalResourceView 객체로 리턴.

DispatcherServlet 동작 과정:

  1. 컨트롤러 실행:

컨트롤러가 요청을 처리하고 뷰 이름과 모델 데이터를 반환.


@Controller
public class WelcomeController {
    @RequestMapping("/welcome")
    public String welcome(Model model, @RequestParam(value = "user", required = false) String user) {
        model.addAttribute("message", "Welcome, " + user + "!");
        return "welcome";
    }
}
  1. ModelAndView 처리: DispatcherServlet은 HandlerAdapter를 통해 ModelAndView 객체를 받음. ModelAndView 객체에는 뷰 이름과 모델 데이터가 포함됨.

  2. ViewResolver 호출: DispatcherServlet은 ViewResolver에게 뷰 이름에 해당하는 View 객체를 요청. InternalResourceViewResolver는 "prefix+뷰이름+suffix" 경로의 JSP 파일을 사용하는 InternalResourceView 객체를 리턴. 예: 뷰 이름 "welcome" -> "WEB-INF/view/welcome.jsp" 경로의 JSP 파일.

  3. 응답 생성: DispatcherServlet은 InternalResourceView 객체에 응답 생성을 요청. InternalResourceView 객체는 JSP 코드를 실행하여 응답 결과를 생성. 모델에 담긴 데이터(예: "message" 속성)는 View 객체에 Map 형식으로 전달되어 JSP에서 사용됨.

  4. 결과: JSP는 전달받은 모델 데이터를 사용하여 알맞은 응답 결과를 생성하고 클라이언트에 전송. 예: "Welcome, user!" 메시지가 포함된 JSP 페이지가 렌더링되어 클라이언트에게 전송.

6.디폴트 핸들러와 HandlerMapping의 우선순위

디폴트 핸들러란?

디폴트 핸들러는 어떠한 HandlerMapping에도 매핑되지 않는 요청을 처리하는 핸들러이다. 특정 URL 패턴을 처리하지 않는 나머지 모든 요청을 처리할 수 있다. 따라서 HandlerMapping의 우선순위가 디폴트 핸들러보다 높다.

DispatcherServlet의 동작 과정:

  1. 요청 수신: 웹 브라우저로부터 요청이 들어오면 DispatcherServlet이 이를 받는다.

  2. 핸들러 검색: DispatcherServlet은 요청 URL을 기반으로 등록된 HandlerMapping 빈을 사용하여 적절한 핸들러를 찾는다.

  3. 핸들러 실행: 적절한 핸들러가 발견되면, DispatcherServlet은 해당 핸들러를 실행한다.

  4. 디폴트 핸들러 실행: 만약 어떤 HandlerMapping에서도 핸들러를 찾지 못한 경우, 디폴트 핸들러가 요청을 처리한다.

-> 이렇게 DispatcherServlet은 우선순위에 따라 핸들러를 검색하고 실행하여, 웹 애플리케이션의 요청을 적절히 처리한다.

Chapter 11: MVC 1 : 요청 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델

1.요청 매핑 어노테이션을 이용한 경로 매핑

웹 어플리케이션 개발 하는 것은 다음 코드를 작성하는 것이다.

  1. 특정 요청 URL을 처리할 코드 작성
  2. 처리 결과를 HTML 등의 형식으로 응답하는 코드 작성

첫 번째 작업은 @Controller 애노테이션을 사용한 컨트롤러 클래스를 이용해서 구현한다.

@Controller
public class YewonController {
    @GetMapping("/yewon")
    public String yewon(Model model, @RequestParam(value = "name", required = false) String name) {
        model.addAttribute("greeting", "Hello " + name);
        return "yewon";
    }
}

컨트롤러 클래스는 요청 매핑 애노테이션(@RequestMapping, @GetMapping 등)을 사용하여 메서드가 처리할 요청 경로를 지정한다. 위 코드의 YewonController 클래스는 @GetMapping을 사용하여 "/yewon" 요청 경로를 yewon() 메서드가 처리하도록 설정하고 있다.

여러 요청 매핑 메서드

컨트롤러 클래스에서 여러 요청 경로를 처리하는 메서드를 정의할 수 있다. 예를 들어, 하나의 컨트롤러 클래스를 만들고 여러 메서드에서 각 요청 경로를 처리할 수 있다:

@Controller
public class MyController {
    @GetMapping("/page1")
    public String handlePage1(Model model) {
        model.addAttribute("message", "This is Page 1");
        return "page1";
    }

    @GetMapping("/page2")
    public String handlePage2(Model model) {
        model.addAttribute("message", "This is Page 2");
        return "page2";
    }
}

위 코드는 각각 "/page1"과 "/page2" 경로를 처리하는 두 개의 메서드를 가지고 있다.

중복된 경로를 클래스 수준으로 리팩토링

메서드마다 중복되는 경로가 있는 경우, 클래스에 공통 경로를 지정할 수 있다. 이렇게 하면 코드가 더 간결해진다:

@Controller
@RequestMapping("/pages")
public class PagesController {
    @GetMapping("/page1")
    public String handlePage1(Model model) {
        model.addAttribute("message", "This is Page 1");
        return "page1";
    }

    @GetMapping("/page2")
    public String handlePage2(Model model) {
        model.addAttribute("message", "This is Page 2");
        return "page2";
    }
}

위 코드에서 @RequestMapping("/pages")를 클래스 수준에 적용하여 "/pages" 경로가 모든 메서드에 공통으로 적용되도록 했다. 따라서, 각 메서드는 "/pages/page1"과 "/pages/page2" 경로를 처리하게 된다.

이렇게 하면 경로가 중복되지 않는다.

2. GET과 POST의 구분: @GetMApping, @GetPost

요청 방식에 따른 매핑

스프링 MVC에서는 기본 설정으로 @RequestMapping 애노테이션을 사용하면 GET, POST 방식을 구분하지 않고 모든 요청을 처리한다. 특정 HTTP 메소드만 처리하려면 @PostMapping이나 @GetMapping 같은 애노테이션을 사용하여 제한할 수 있다.

POST 방식 요청만 처리하고 싶다면 @PostMapping을 사용:

@Controller
public class YewonController {

    @PostMapping("/yewon/submit")
    public String handleSubmit() {
        return "yewon/submit";
    }
}

handleSubmit 메서드는 POST 방식의 /yewon/submit 요청 경로만 처리하며, 같은 요청 경로의 GET 요청은 처리하지 않는다.

특정 경로의 GET 방식 요청만 처리하고 싶다면 @GetMapping을 사용:

@Controller
public class YewonController {

    @GetMapping("/yewon/form")
    public String showForm() {
        return "yewon/form";
    }
}

showForm 메서드는 GET 방식의 /yewon/form 요청 경로만 처리한다.

3. 요청 파라미터 접근

HTML 폼을 통해 요청 파라미터를 전달하고 이를 컨트롤러에서 처리하는 방법은 두 가지가 있다.

약관 동의 체크박스를 포함한 폼 + 이를 처리하는 코드로 이해해보자

HTML 폼 예시

<form action="/yewon/step2" method="post">
    <label>
        <input type="checkbox" name="agree" value="true"> 약관 동의
    </label>
    <input type="submit" value="다음 단계">
</form>

위 폼은 사용자가 약관에 동의할 경우 값이 "true"인 agree 요청 파라미터를 POST 방식으로 전송.

방법 1: HttpServletRequest를 사용하는 방법

@PostMapping("/yewon/step2")
public String handleStep2(HttpServletRequest request) {
    String agreeParam = request.getParameter("agree");
    if (agreeParam == null || !agreeParam.equals("true")) {
        return "yewon/step1";
    }
    return "yewon/step2";
}

방법 2: @RequestParam 애노테이션을 사용하는 방법

@PostMapping("/yewon/step2")
public String handleStep2(@RequestParam(value="agree", defaultValue="false") Boolean agree) {
    if (!agree) {
        return "yewon/step1";
    }
    return "yewon/step2";
}

요약

  1. HttpServletRequest 사용:

    • 요청 파라미터를 직접 가져와서 처리.
    • 예시: request.getParameter("agree")
  2. @RequestParam 사용:

    • 애노테이션으로 요청 파라미터를 간편하게 받음.
    • 예시: @RequestParam(value="agree", defaultValue="false") Boolean agree

두 방법 모두 요청 파라미터를 처리하고, 약관에 동의하지 않았을 경우 다시 동의 폼을 보여주고, 동의했을 경우 다음 단계로 넘어간다.

4. 리다이렉트 처리

handleStep2()` 메소드는 POST 요청만 처리합니다. 따라서, 웹브라우저에서 직접 URL을 입력해 GET 요청을 보내면 HTTP 405 - Method Not Allowed 에러가 발생하게 됨.

잘못된 전송 방식으로 요청이 왔을 때 에러 화면보다 알맞은 경로로 리다이렉트 하는 것이 더 좋을 수 있다. 컨트롤러에서 특정 페이지로 리다이렉트 시키는 방법은 "redirect:경로" 를 뷰 이름으로 리턴하여 처리하는 것이다.

  1. 예시 코드:

    @Controller
    public class RegisterController {
    
       @GetMapping("/register/step2")
       public String handleStep2Get() {
           return "redirect:/register/step1";
       }
    }
    • handleStep2Get() 메소드는 GET 요청을 처리하고, 클라이언트를 /register/step1으로 리다이렉트합니다.

요약

5. 커맨드 객체를 이용해 요청 파리미터 사용하기

커맨드 객체를 이용한 요청 파라미터 처리

문제

폼에서 email, name, password, confirmPassword와 같은 요청 파라미터를 전송할 때, HttpServletRequest를 사용하면 코드가 길어지고 관리가 어려워질 수 있음.

해결 방법

스프링 MVC는 요청 파라미터를 커맨드 객체에 자동으로 바인딩할 수 있는 기능을 제공 코드의 복잡성을 줄이고 유지보수성을 향상시킬 수 있음.

커맨드 객체 사용 예시

  1. 커맨드 객체 클래스
@Getter
@Setter
public class RegisterRequest {
    private String email;
    private String name;
    private String password;
    private String confirmPassword;
}

@Getter@Setter는 롬복(Lombok) 라이브러리의 어노테이션으로, 자동으로 게터와 세터 메서드를 생성.

  1. 컨트롤러 메소드
@PostMapping("/register/step3")
public String handleStep3(RegisterRequest registerRequest) {
    // 커맨드 객체의 필드 값에 자동으로 요청 파라미터가 바인딩됨
    String email = registerRequest.getEmail();
    String name = registerRequest.getName();
    String password = registerRequest.getPassword();
    String confirmPassword = registerRequest.getConfirmPassword();

    // 추가적인 로직 처리
    if (!password.equals(confirmPassword)) {
        return "registerForm";
    }

    return "registrationSuccess";
}

RegisterRequest 객체의 필드에 요청 파라미터 값이 자동으로 바인딩됨. 이를 통해 코드가 간결해지고 요청 파라미터의 처리가 더 쉬워짐

6. 뷰 JSP 코드에서 커맨드 객체 사용하기

7. @ModelAttribute 어노테이션으로 커맨드 객체 속성 이름 변경

커맨드 객체와 뷰에서의 속성 이름 변경

스프링 MVC는 기본적으로 커맨드 객체의 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 뷰에 전달한다. 만약 다른 이름으로 접근하고 싶다면, @ModelAttribute 애노테이션을 사용하여 이름을 변경할 수 있다.

컨트롤러 메소드

@PostMapping("/login")
public String handleLogin(@ModelAttribute("loginData") LoginForm loginForm) {

    String username = loginForm.getUsername();
    String password = loginForm.getPassword();

    .
    .
    .
    return "loginSuccess";
}

@ModelAttribute("loginData")를 사용하여, 뷰에서는 loginData라는 이름으로 LoginForm 객체에 접근할 수 있다.

요약

이렇게 하면 뷰에서 커맨드 객체에 더 명확하고 일관된 이름으로 접근할 수있게 된다.

8. 커맨드 객체와 스프링 폼 연동

커맨드 객체와 스프링 폼 연동

스프링 MVC는 커맨드 객체와 연동하여 폼을 간편하게 처리할 수 있는 커스텀 태그를 제공한다. 주요 태그는 form:formform:input 이다.

  1. 폼 코드 (JSP)
<form:form action="step3" modelAttribute="registerRequest">
    <form:input path="email" />
    <form:input path="name" />
    <input type="submit" value="Submit" />
</form:form>
  1. 컨트롤러 메소드
@Controller
public class RegisterController {

    @PostMapping("/register/step2")
    public String handleStep2(@RequestParam(value="agree", defaultValue="false") Boolean agree, Model model) {
        if (!agree) {
            return "register/step1";
        }
        model.addAttribute("registerRequest", new RegisterRequest());
        return "register/step2";
    }
}

model.addAttribute("registerRequest", new RegisterRequest());를 통해 모델에 커맨드 객체를 추가함. -> 이 객체가 폼에서 사용됨

이렇게 하면 폼에서 커맨드 객체의 필드에 접근하고, 데이터를 쉽게 바인딩할 수 있다

9. 주요 에러 발생 상황

1. 요청 매핑 애노테이션 관련 에러

2. @RequestParam 및 커맨드 객체 관련 에러

10. Model을 통해 컨트롤러에서 뷰 데이터 전달

Model을 통해 컨트롤러에서 뷰 데이터 전달

컨트롤러는 뷰가 화면을 구성하는 데 필요한 데이터를 제공하기 위해 Model 또는 ModelAndView를 사용.

1. Model 사용

@Controller
public class HaewonController {

    @GetMapping("/greeting")
    public String showGreeting(Model model) {
        model.addAttribute("message", "Hello, yewon!");
        return "greetingView";
    }
}

model.addAttribute("message", "Hello, yewon!");를 통해 message라는 이름으로 데이터를 뷰에 전달

2. ModelAndView 사용

예시

@Controller
@RequestMapping("/welcome")
public class HaewonController {

    @GetMapping
    public ModelAndView showWelcome() {
        ModelAndView mav = new ModelAndView();
        mav.addObject("message", "Welcome, yewon!");
        mav.setViewName("welcomeView");
        return mav;
    }
}

mav.addObject("message", "Welcome, yewon!");로 데이터를 추가하고, mav.setViewName("welcomeView");로 뷰 이름을 설정함

jlhyunii commented 1 month ago

Chapter 8. DB 연동



1. JDBC 프로그래밍의 단점을 보완하는 스프링



자바의 DB 연동


JDBC를 위해 스프링이 제공하는 JdbcTemplate을 사용한다.


JdbcTemplate 클래스


구조적인 반복을 줄이기 위한 방법으로, 템플릿 메서드 패턴과 전략 패턴을 함께 사용한다.

List<Member> results = jdbcTemplate.query(
    "select * from MEMBER where EMAIL = ?",
    new RowMapper<Member>() {
        @Override
        public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
            Member member = new Member(rs.getString("EMAIL"),
                rs.getString("PASSWORD"),
                rs.getString("NAME"),
                rs.getTimestamp("REGRATE"));
            member.setId(rs.getLong("ID"));
            return member;
        }
    },
    email);
return results.isEmpty() ? null : results.get(0);


트랜잭션 관리


데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위이다. 즉, 여러 작업을 진행하다가 문제가 생겼을 경우, 이전 상태로 롤백하기 위해 사용된다.

<용어 정리>

  • 커밋 : 모든 작업이 성공해서 데이터 베이스에 정상 반영되는 것.
  • 롤백 : 작업 중 하나라도 실패해서 이전으로 되돌리는 것.

-> 커밋과 롤백 처리는 스프링이 알아서 해준다. 우리는 트랜잭션 처리를 제외한 핵심 코드에 집중하자!


트랜잭션 기능 사용을 위한 build.gradle


dependencies {
    .
    .
    implementation 'org.springframework:spring-jdbc:5.3.9' // 버전은 달라질 수 있음
    implementation 'org.apache.tomcat:tomcat-jdbc:10.0.8' // 버전은 달라질 수 있음
    implementation 'mysql:mysql-connector-java:8.0.26' // 버전은 달라질 수 있음
    .
    .
}


커넥션 풀


일정 개수의 DB 커넥션을 미리 만들어두는 기법.

장점

  • 커넥션을 미리 생성해두기 때문에 생성하는 시간을 아낄 수 있다.
  • 더 많은 동시 접속자를 처리할 수 있다.

따라서, 실제 서비스 운영 환경에서는 매번 커넥션을 생성하지 않고 커넥션 풀을 사용해서 DB 연결을 관리한다.

DB 커넥션 풀 기능을 제공하는 모듈



2. 프로젝트 준비



DB 테이블 생성


create user 'spring5'@'localhost' identified by 'spring5';

create database spring5fs character set=utf8;

grant all privileges on spring5fs.* to 'spring5'@'localhost';

create table spring5fs.MEMBER (
    ...
) engine=InnoDB character set = utf8;

MySQL은 "utf-8"이 아닌 하이픈(-)이 없는 "utf8"을 사용한다.



3. DataSource 설정



Connection conn = null;
try {
    // dataSource는 생성자나 설정 메서드를 이용해서 주입받음
    conn = dataSource.getConnection();
    ...

스프링이 제공하는 DB 연동 기능은 DataSource를 사용해서 DB Connection을 구한다.

DataSource 클래스는 Tomcat JDBC가 제공한다.


Tomcat JDBC의 주요 프로퍼티


Tomcat JDBC 모듈의 org.apache.tomcat.jdbc.pool.DataSource 클래스는 커넥션 풀 기능을 제공하는 DataSource 구현 클래스이다. DataSource 클래스는 커넥션을 몇 개 만들지 지정할 수 있는 메서드를 제공한다.


커넥션의 상태

  • 활성(active) 상태 : 커넥션 풀에 커넥션을 요청 -> dataSource.getConnection()
  • 유휴(idle) 상태 : 커넥션을 다시 커넥션 풀에 반환 -> close() 메서드 사용


설정 메서드 설명 기본값
setInitialSize(int) 커넥션 풀을 초기화할 때 생성할 초기 커넥션 개수 지정 10
setMaxActive(int) 커넥션 풀에서 가져올 수 있는 최대 커넥션 개수 지정 100
setMaxIdle(int) 커넥션 풀에 유지할 수 있는 최대 커넥션 개수 지정 100
setMaxWait(int) 커넥션 풀에서 커넥션을 가져올 때 대기할 최대 시간을 밀리초 단위로 지정 30000밀리초
setTestWhileIdle(boolean) 커넥션이 풀에 유휴 상태로 있는 동안에 검사할지 여부 지정 false
setMinEvictableTimeMillis(int) 커넥션 풀에 유휴 상태로 유지할 최소 시간을 밀리초 단위로 지정 60000밀리초
setTimeBetweenEvictableRunsMillis(int) 커넥션 풀의 유휴 커넥션을 검사할 주기를 밀리초 단위로 지정 5000밀리초,
1초이하X



4. JdbcTemplate을 이용한 쿼리 실행



스프링을 사용하면 DataSource나 Connection, Statement, ResultSet을 직접 사용하지 않고 JdbcTemplate을 이용해서 편리하게 쿼리를 실행할 수 있다.


조회 쿼리 실행


JdbcTemplate 클래스는 SELECT 쿼리 실행을 위한 query() 메서드를 제공한다.


query() 메서드


RowMapper 인터페이스

package.org.springframework.jdbc.core;

public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}


임의 클래스를 이용해서 RowMapper의 객체 전달

List<Member> results = jdbcTemplate.query(
    "select * from MEMBER where EMAIL = ? and NAME = ?",
    new RowMapper<Member>() {...코드생략},
    email, name); // 물음표 개수만큼 해당되는 값 전달


queryForObject() 메서드


쿼리 실행 결과 행이 한 개인 경우에 사용할 수 있는 메서드다.

.
.
public int count() {
    integer count = jdbcTemplate.queryForObject(
        "select count(*) from MEMBER", Integer.class);
    return count;
}
.
.


만약 쿼리 실행 결과 행이 없거나 두 개 이상이면?

-> IncorrectResultSizeDataAccessException이 발생한다.

행의 개수가 0이면?

-> EmptyResultDataAccessException이 발생한다.

따라서, 결과 행이 정확히 한 개가 아니면 queryForObject() 메서드 대신 query() 메서드를 사용하자!


변경 쿼리 실행 - update() 메서드


INSERT, UPDATE, DELETE 쿼리는 update() 메서드를 사용한다.

update() 메서드는 쿼리 실행 결과로 변경된 행의 개수를 리턴한다.


PreparedStatementCreator를 이용한 쿼리 실행


PreparedStatement의 set 메서드를 사용해 직접 인덱스 파라미터의 값을 설정할 때도 있다. 이 경우 PreparedStatementCreator를 인자로 받는 메서드를 이용해서 직접 PreparedStatement를 생성하고 설정해야 한다.

PreparedStatementCreator 인터페이스

.
.
public interface PreparedStatementCreator {
    PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}
.
.


PreparedStatementCreator 인터페이스를 파라미터로 갖는 메서드

KeyHolder를 사용하면 자동으로 생성된 키값을 구할 수 있다.


5. 익셉션



DB 연동 과정에서 발생 가능한 익셉션


1. CannotGetJdbcConnectionException


2. BadSqlGrammerException & MySQLSyntaxErrorException



6. 스프링의 익셉션 변환 처리



Jdbc API를 사용하는 과정에서 SQLException이 발생하면 이 익셉션을 알맞은 DataAccessException으로 변환해서 발생한다.

예를 들면, MySQL용 JDBC 드라이버는 SQL 문법이 잘못된 경우 SQLException을 상속받은 MySQLSyntaxErrorException을 발생시키는데,

JdbcTemplate은 이 익셉션을 DataAccessException을 상속받은 BadSqlGrammerException으로 변환한다.

왜?

연동 기술에 상관없이 동일하게 익셉션을 처리할 수 있도록 하기 위함이다. 즉, 스프링이 제공하는 익셉션으로 변환함으로써 구현 기술에 상관없이 동일한 코드로 익셉션을 처리하는 것이다.



7. 트랜잭션 처리



트랜잭션 (transaction)


두 개 이상의 쿼리를 한 작업으로 실행해야 할 때 사용한다.


Connection conn = null;
try {
    conn = DriverManager.getConnection(jdbcUrl, user, pw_;
    conn.setAutoCommit(false); // 트랜잭션 범위 시작
    ... // 쿼리 실행
    conn.commit(); // 트랜잭션 범위 종료 : 커밋
} catch(SQLException ex) {
    if (conn!=null)
        // 트랜잭션 범위 종료 : 롤백
        try {conn.rollback();} catch (SQLException e) {}
} finally {
    if (conn != null)
        try {conn.close();} catch (SQLException e) {}

스프링이 제공하는 트랜잭션 기능을 사용하면 중복이 없는 매우 간단한 코드로 트랜잭션 범위를 지정할 수 있다.


@Transactional


...

@Transactional
public void changePassword(String email, String oldPwd, String newPwd) {
    Member member = memberDao.selectByEmail(email);
    if(member == null)
        throw new MemberNotFoundException();
    member.changePassword(oldPwd, newPwd);

    memberDao.update(member);


@Transactional 애노테이션을 제대로 동작하려면?


...

@Configuration
@EnableTransactionalManagement
public class AppCtx {
    .
    .
    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource());
        return tm;
    }
    .
    .
}

1. 플랫폼 트랜잭션 매니저(PlatformTransactionManager) 빈 설정

2. @EnableTransactionManagement


즉, @Transactional 애노테이션은 트랜잭션 범위에서 실행하고 싶은 스프링 빈 객체의 메서드에 붙이고, @EnableTransactionManagement 애노테이션은 트랜잭션 범위에서 메서드를 실행하기 위한 기능을 활성화할 때 붙인다.


로그 메시지


실제로 트랜잭션이 시작되고 커밋되는지 확인할 수 없다. 이를 확인하기 위해 build.gradle 파일에 Logback 모듈을 추가하자!


그렇다면, 트랜잭션을 시작하고 커밋하고 롤백하는 것은 누가 어떻게 처리하는 걸까?


@Transactional과 프록시


@Transactional 애노테이션이 적용된 빈 객체를 찾아서 알맞은 프록시 객체를 생성한다.

ChangePasswordService 클래스의 메서드에 @Transactional 애노테이션이 적용되어 있다고 하자.


프록시 객체가 @Transactional 애노테이션이 붙은 메서드를 호출하면?

  • 1.1 : PlatformTransactionManager를 사용해서 트랜잭션을 시작한다.
  • 1.2~1.3 : 실제 객체의 메서드를 호출한다.
  • 1.4 : 성공적으로 실행되면 트랜잭션을 커밋한다.


@Transactional 적용 메서드의 롤백 처리


롤백을 처리하는 주체 또한 프록시 객체이다.

실제로 @Transactional을 처리하기 위한 프록시 객체는 원본 객체의 메서드를 실행하는 과정에서 RuntimeException이 발생하면 트랜잭션을 롤백한다.


프록시가 트랜잭션을 롤백하는 경우


왜?? 모두 RuntimeException을 상속받기 때문!


만약 RuntimeException을 상속하지 않는 Exception이 발생하면 어떻게 될까?


@Transactional의 rollbackFor 속성


@Transactional(rollbackFor = SQLException.class)
public void someMethod() {
    ...
}


@Transactional의 noRollbackFor 속성


지정한 익셉션이 발생해도 롤백시키지 않고 커밋할 익셉션 타입을 지정할 때 사용한다.


@Transactional의 주요 속성


속성 타입 설명 기본값
value String 트랜잭션을 관리할 때 사용할 PlatformTransactionManager 빈의 이름 지정 " "
propagation Propagation 트랜잭션 전파 타입 지정 Propagation.REQUIRED
isolation Isolation 트랜잭션 격리 레벨 지정 Isolation.DEFAULT
timeout int 트랜잭션 제한 시간 지정 -1


1. value 속성

@Transactional 애노테이션의 value 속성값이 없으면 등록된 빈 중에서 타입이 PlatformTransactionManager인 빈을 사용한다.


2. Propagation 속성

트랜잭션 필요 여부 설명
REQUIRED 현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 사용,
존재하지 않으면 새로운 트랜잭션 생성
MANDATORY 진행 중인 트랜잭션이 존재하지 않을 경우 익셉션 발생
REQUIRES_NEW 항상 새로운 트랜잭션을 시작. 진행 중인 트랜잭션이 존재하면 기존 트랜잭션을 중단하고, 새로 시작된 트랜잭션이 종료된 뒤에 기존 트랜잭션 계속
SUPPORTS 굳이 필요로 하지는 않지만, 진행 중인 트랜잭션이 존재하면 트랜잭션을 사용
NOT_SUPPORTED X 진행 중인 트랜잭션이 존재할 경우 트랜잭션은 일시 중지되고 메서드 실행이 종료된 후에 트랜잭션을 계속 진행
NEVER X 진행 중인 트랜잭션이 존재할 경우 익셉션 발생
NESTED 진행 중인 트랜잭션이 존재하면 기존 트랜잭션에 중첩된 트랜잭션에서 메서드를 실행,
존재하지 않으면 REQUIRED와 동일하게 동작


3. Isolation 속성

트랜잭션 격리 레벨은 동시에 DB에 접근할 때 그 접근을 어떻게 제어할지에 대한 설정을 다룬다.

설명
DEFAULT 기본 설정을 사용
READ_UNCOMMITTED 다른 트랜잭션이 커밋하지 않은 데이터 읽기 가능
READ_COMMITTED 다른 트랜잭션이 커밋한 데이터 읽기 가능
REPEATABLE_READ 처음에 읽어 온 데이터와 두 번째 읽어 온 데이터가 동일한 값을 가짐
SERIALIZABLE 동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션 수행 불가능


@EnableTransactionManagement의 주요 속성


속성 설명 기본값
proxyTargetClass 클래스를 이용해서 프록시를 생성할지 여부 지정 false로서 인터페이스를 이용해 프록시 생성
order AOP 적용 순서 지정 가장 낮은 우선순위에 해당하는 int의 최댓값


JdbcTemplate은 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 범위에서 쿼리를 실행한다.


public class ChangePasswordService {
    ...

    @Transactional
    public void changePassword(String email, String oldPwd, String newPwd) {
        ...
    }
}

public class MemberDao {
    ...

    // @Transactional 없음
    public void update(Member member) {
        ...
    }
}
genius00hwan commented 1 month ago