-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[5주차] 해원, 은채, 태균, 수진 #4
Comments
Chapter15 간단한 웹 어플리케이션 구조1.웹 어플리케이션의 구성 요성웹 어플리케이션을 개발할 때 사용하는 전형적인 구조는 다음 요소를 포함한다.
@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.getCurrendPassword(),
pwdCmd.getNewPassword());
return "edit/changePwd";
}catch(IdPasswordNotMatchingException e){
errors.rejectValue("currentPassword", "notMatching");
return "edit/changePwdForm";
}
} 위에 코드는 ChangePasswordService에 비밀번호 변경 처리를 위임했다
2.서비스의 구현서비스는 핵심이 되는 기능의 로직을 제공한다. 예를 들어 비밀번호 변경 기능은 아래와 같은 로직을 서비스에서 수행한다.
->웹 어플리케이션을 사용하든 명령행에서 실행하든 비밀번호 변경 기능을 위해서 서비스는 동일한 로직을 수행하는데 @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);
} 같은 데이터를 사용하는 기능들을 한 개의 서비스 클래스에 모아서 구현할 수 있다. 예를 들어 회원 가입 기능과 비밀번호 변경 기능은 모두 회원에 대한 기능이므로 다음과 같이 MemberService 클래스에 기능을 구현할 수 있다. public class MemberService{
@Transactional
public void regist(RegisterRequest req){...}
@Transactional
public void changePassword(String email, String lodPwd, String newPwd){...}
} 회원가입 기능은 RegisterRequest 클래스를 파라미터로 사용했다. 필요한 데이터를 전달받기 위해 별도 타입을 만들면 스프링 MVC의 커맨드 객체로 해당 타입을 사용할 수 있어 편하다. 회원 가입 요청을 처리하는 컨트롤러 클래스의 코드는 다음과 같이 서비스 메서드의 입력 파라미터로 사용되는 타입을 커맨드 객체로 사용했다. @PostMapping("/register/step3")
public String handleSteo3(RegisterRequest regReq, Errors errors){
...
memberRegisterService.regist(regReq);
...
} 비밀번호 변경의 changePassword() 메서드처럼 웹 요청 파라미터를 커맨드 객체로 받고 커맨드 객체의 프로퍼티를 서비스 메서드에 인자로 전달할 수도 있다. @RequestMapping(method=RequestMethod.POST)
public String submit(@ModelAttribute("command") ChangePwdCommand pwdCmd, Errors errors, HttpSession session){
...
changePasswordService.changePassword(
authInfo.getEmail(),
pwdCmd.getCurrentPassword(),
pwdCmd.getNewPassword()
);
...
}
3.컨트롤러에서 DAO접근서비스 메서드에서는 어떤 로직도 수행하지 않고 단순히 DAO의 메서드만 호출하고 끝나는 경우도 있다. 예를 들어 회원 데이터 조회를 위한 서비스 메서드를 다음과 같이 구현하곤 한다. public class MemberSerivce{
...
public Member getMember(Long id){
return memberDao.selectById(id);
}
} 여기서 MemberService 클래스의 getMember() 메서드는 MemberDao의 selectByID()만 실행할 뿐 추가 로직은 없다. 컨트롤러 클래스는 이 서비스 메서드를 통해 회원 정보를 구한다. @RequestMapping("/member/detail/{id}")
public String detail(@PathVariable("id") Long id, Model model){
//사실상 DAO를 직접 호출하는 것과 동일
Member member = memberService.getMember(id);
//Member member = memberDao.selectByEmail(id);
if(member==null)
return "member/noFound";
model.addAttribute("member",member);
return "member/memberDatail";
} 4.패키지 구성패키지의 구성을 조금 더 정확하게 분리하면 아래 그림처럼 '웹 요청을 처리하기 위한 것'과 '기능을 제공하기 위한 것'으로 구분할 수 있다.
이 영역에는 실제 비즈니스 로직과 데이터 처리를 담당하는 클래스들이 위치합니다:
:도메인 주도 설계는 복잡한 애플리케이션의 구조를 체계적으로 관리하기 위한 방법론입니다. DDD에서는 애플리케이션을 UI, 서비스, 도메인, 인프라의 네 영역으로 나눕니다
역할: 사용자와의 상호작용을 담당합니다. 구성 요소: 컨트롤러, 뷰 예시: web.member 패키지
역할: 도메인 로직을 조정하고 트랜잭션을 관리합니다. 구성 요소: 애플리케이션 서비스 예시: domain.member.service 패키지
역할: 핵심 비즈니스 로직과 규칙을 포함합니다. 구성 요소: 도메인 모델, 엔티티, 밸류 오브젝트, 도메인 이벤트, 애그리게잇 예시: domain.member.model 패키지
역할: 데이터베이스와의 상호작용을 담당합니다. 구성 요소: 리포지토리, DAO, 외부 시스템과의 통신 예시: domain.member.dao 패키지
chapter16 JSON 응답과 요청 처리1.JSON 개요Jackson은 자바 객체와 JSON 형식 문자열 간 변환을 처리하는 라이브러리이다.스프링 MVC에서 Jackson 라이브러리를 이용해서 자바 객체를 JSON으로 변환하려면 클래스 패스에 Jackson 라이브러리를 추가하면 된다. 2.JSON 의존 설정Jackson은 아래 그림과 같이 자바 객체와 JSON 사이의 변환을 처리한다. 3.@RestController로 JSON 형식 응답스프링 MVC에서 JSON 형식으로 데이터를 응답하기 위해선 @controller 대신 @RestController 어노테이션을 사용하면 된다.@RestController
public class RestMemberController{
private MemberDao memberDao;
private MemberRegisterService registerService;
@GetMapping("/api/members")
public List<Member> members(){
return memberDao.selectAll();
}
@GetMapping("/api/members/{id}")
public Member member(@PathVariable Long id, HttpServletResponse response) throws IOException{
...
}
...
}
@Controller
public class RestMemberController{
private MemberDao memberDao;
private MemberRegisterService registerService;
@RequestMapping(path="/api/members", method=RequestMethod.GET)
@ResponseBody
public List<Member> members(){
return memberDao.selectAll();
}
}
응답 결과에 password가 포함되어 있으면 안되기 때문에 응답 결과에서 제외시켜야 한다. Jackson이 제공하는 @JsonIgnore 어노테이션을 사용하면 이를 간단히 처리할 수 있다. 다음과 같이 JSON 응답에 포함시키지 않을 대상에 어노테이션을 붙인다. public class Member{
private Long id;
private String email;
@JsonIgnore
private String password;
private String name;
private LocalDateTime registerDateTime;
}
Jackson에서 날짜나 시간 값을 특정한 형식으로 표현하는 가장 쉬운 방법은 @jsonformat을 사용하는 것이다. public class Member{
private Long id;
private String email;
private String name;
@JsonFormat(pattern="yyyyMMddHHmmss")
private LocalDateTime registerDateTime;
} Json 응답 결과는 다음과 같다. {
"id":1,
"email":"[email protected]",
"name:"spring",
"registerDateTime":"202208150130"
}
스프링 MVC는 자바 객체를 HTTP 응답으로 변환할 때 HttpMessageConverter라는 것을 사용한다. JSON으로 변환할 때 사용하는 컨버터를 새롭게 등록해서 날짜 형식을 원하는 형태로 변환하도록 설정하면 모든 날짜 형식에 동일한 변환 규칙을 적용할 수 있다. @Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfiguration{
...
@Override
public viod extendMessageConverter{
List<HttpMessageConverter<?>> converters){
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
converters.add(0,new MappingJackson2HttpMessageConverter(objectMapper));
}
}
}
응답 헤더의 Content-type이 application/json인 것을 알 수 있다.
4.@RequestBody로 JSON 요청 처리POST, PUT 방식을 사용하면 name=이름&age=20과 같은 쿼리 문자열 형식이 아니라 다음과 같은 JSON 형식의 데이터를 요청 데이터로 전송할 수 있다.
JSON 형식으로 전송된 요청 데이터를 커맨드 객체로 전달받는 방법은 커맨드 객체에 @RequestBody 어노테이션을 붙이면 된다. @RestController{
...
@PostMapping("/api/members")
public void new Member(@RequestBody @Valid RegisterRequest regReq, HttpServletResponse response){
...
}
}
특정 패턴을 가진 문자열을 LocalDateTime이나 Date 타입으로 변환하고 싶다면 @jsonformat 어노테이션의 pattern 속성을 이용해서 패턴을 지정한다. @JsonFormat(pattern="yyyyMMddHHmmss")
private LocalDateTime birthDateTime;
@JsonFormat(pattern="yyyyMMdd HHmmss")
private Date birthDate;
newMember()의 regReq 파라미터에 @Valid 어노테이션이 붙어있다. JSON 형식으로 전송한 데이터를 변환한 객체도 동일한 방식으로 @Valid 어노테이션이나 별도 Validator를 이용해서 검증할 수 있다. Validator를 사용할 경우 직접 상태 코드를 처리해야 한다. import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;
@RestController
@Validated
public class RestMemberController {
// ... 필드 선언 및 의존성 주입
@PostMapping("/api/members")
public void newMember(@RequestBody RegisterRequest regReq, HttpServletResponse response) {
// 커스텀 Validator를 사용하여 요청 데이터를 검증
if (!isValid(regReq)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid request data");
}
// 요청 처리 로직
}
private boolean isValid(RegisterRequest regReq) {
// 검증 로직 구현
return true; // 실제 검증 로직에 따라 수정
}
} 위 코드에서는 ResponseStatusException을 사용하여 상태 코드와 오류 메시지를 설정합니다 . isValid 메서드는 요청 데이터를 검증하는 커스텀 로직을 구현합니다. 5.ResponseEntity로 객체 리턴하고 응답 코드 지정하기지금까지는 상태 코드를 지정하기 위해 HttpServletResponse의 setStatus(), sendError()를 사용했다. 문제는 HttpServletResponse를 이용해서 404 응답을 하면 JSON 형식이 아닌 서버가 기본으로 제공하는 HTML을 응답 결과로 제공한다는 점이다. API를 호출하는 입장에서 JSON과 HTML을 모두 처리하는 것은 부담스럽다. 처리에 실패한 경우 HTML 응답 데이터 대신에 JSON 형식의 응답 데이터를 전송해야 API 호출 프로그램이 일관된 방법으로 응답을 처리할 수 있다.
정상인 경우와 비정상인 경우 모두 JSON 응답을 전송하는 방법은 ResponseEntity를 사용하는 것이다. 에러 상황일 때 응답으로 사용할 ErrorResponse 클래스이다. public class ErrorResponse{
private String message;
public ErrorResponse(String message){
this.message = message;
}
public String getMessage(){
return message;
}
} } @RestController
public class RestMemberController{
...
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable Long id){
Member member = memberDao.selectById(id);
if(member==null){
//ErrorResponse를 body로 지정해서,
//ErrorResponse를 JSON으로 변환한다.
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
//member를 body로 지정해서, member 객체를 JSON으로 변환한다.
return ResponseEntity.status(HttpStatus.OK).body(member);
}
} 스프링 MVC는 리턴 타입이 ResponseEntity이면 body로 지정한 객체를 사용해서 변환을 처리한다. ResponseEntity의 status로 지정한 값을 응답 상태 코드로 사용한다. 존재하지 않는 ID를 이용해서 실행한 결과 404 상태 코드와 함께 JSON 형식으로 응답 데이터를 전송한 것을 확인할 수 있다. ResponseEntity를 생성하는 기본 방법은 status와 body를 이용해서 상태 코드와 JSON으로 변환할 객체를 지정하는 것이다.
200(OK) 응답 코드와 몸체 데이터를 생성할 경우 다음과 같이 ok() 메서드를 이용할 수 있다.
만약 몸체 내용이 없다면 다음과 같이 body를 지정하지 않고 build()로 바로 생성한다.
몸체 내용이 없는 경우 status() 메서드 대신에 다음과 같이 관련 메서드를 사용해도 된다.
newMember()에서 201(Created) 상태 코드와 Location 헤더를 함께 전송하는 방법 //1
response.setHeader("Location", "/api/members/"+newMemberId);
response.setStatus(HttpServletResponse.SC_CREATED);
//2
URI uri = URI.create("/api/members/"+newMemberId);
return ResponseEntity.created(uri).build();
앞선 코드는 member가 존재하지 않을 때 기본 HTML 에러 응답 대신에 JSON 응답을 제공하기 위해 ResponseEntity를 사용했다. 그런데 회원이 존재하지 않을 때 404 상태 코드를 응답해야 하는 기능이 많으면 에러 응답을 위해 ResponseEntity를 생성하는 코드 중복이 발생한다. 이때 @ExceptionHandler 어노테이션을 적용한 메서드에서 에러 응답을 처리하도록 구현하면 중복을 없앨 수 있다. @GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable Long id){
Member member = memberDao.selectById(id);
if(member==null){
throw new MemberNotFoundException();
}
return member;
}
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData(){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member");
} 위 코드의 member() 메서드는 Member 자체를 리턴한다. 회원 데이터가 존재하면 Member 객체를 리턴하므로 JSON으로 변환한 결과를 응답한다. 회원 데이터가 존재하지 않으면 MemberNotFoundException을 발생한다. 이 익셉션이 발생하면 @ExceptionHandler 어노테이션을 사용한 handleNoData() 메서드가 에러를 처리한다. 404 상태 코드와 ErrorResponse 객체를 몸체로 갖는 ResponseEntity를 반환한다. 즉, MemberNotFoundException가 발생하면 상태코드가 404이고 몸체가 JSON 형식인 응답을 전송한다.
@RestControllerAdvice("controller")
public class ApiExceptionAdvice{
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData(){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member");
}
}
@Valid 어노테이션을 붙인 커맨드 객체가 값 검증에 실패하면 400 상태 코드를 응답한다. @PostMapping("/api/members")
public ResponseEntity<Object> newMember(@RequestBody @Valid RegisterRequest regReq){
...
} 문제는 HttpServletResponse를 이용해서 상태 코드를 응답했을 때와 마찬가지로 HTML 응답을 전송한다. @Valid 어노테이션을 이용한 검증을 실패했을 때 HTML 응답 대신 JSON 형식의 응답을 제공하고 싶으면 Errors 타입 파라미터를 추가해서 직접 에러 응답을 생성하면 된다. @PostMapping("/api/members")
public ResponseEntity<Object> newMember(@RequestBody @Valid RegisterRequest regReq, Errors errors){
if(errors.hasErrors()){
String errorCodes=errors.getAllErrors()//List<ObjectError>
.stream()
.map(error->error.getCodes()[0])//error는 ObjectError
.collect(Collectors.joining(","));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("errorCodes="+errorCodes);
}
...
} hasErrors() 메서드를 이용해서 검증 에러가 있는지 확인한다. 검증에러가 존재하면 모든 에러 정보를 구하고 각 에러 코드 값을 연결한 문자열을 생성해서 errorCodes 변수에 할당한다. 이처럼 코드를 수정한 후 검증에 실패하는 데이터를 전송하면 JSON 응답이 오는 것을 확인할 수 있다. @RequestBody를 붙인 경우 @Valid를 붙인 객체의 검증에 실패했을 때 Errors 타입 파라미터가 존재하지 않으면 MethodArgumentNotValidException이 발생한다. 따라서 다음과 같이 @ExceptionHandler 어노테이션을 이용해서 검증 실패시 에러 응답을 생성해도 된다. @RestControllerAdvice("controller")
public class ApiExceptionAdvice{
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleBindException(MethodArgumentNotValidException ex){
String errorCodes=ex.getBindingResult().getAllErrors()
.stream()
.map(error->error.getCodes()[0])
.collect(Collectors.joining(","));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("errorCodes="+errorCodes);
}
} Chapter17 프로필과 프로퍼티 파일1.프로필프로필(Profile)은 스프링에서 환경별로 설정을 다르게 관리할 수 있도록 해주는 기능입니다. 이를 통해 개발 환경과 실제 서비스 환경의 설정을 분리하여 관리할 수 있으며, 실수를 방지하고 효율적인 개발과 배포를 도와줍니다. 프로필의 개념프로필은 논리적인 이름으로서 설정 집합에 프로필을 지정할 수 있다. 스프링 컨테이너는 설정 집합 중에서 지정한 이름을 사용하는 프로필을 선택하고 해당 프로필에 속한 설정을 이용해서 컨테이너를 초기화할 수 있다. 프로필 적용의 예시예를 들어 로컬 개발 환경을 위한 DataSource 설정을 "dev" 프로필로 지정하고 실 서비스를 위한 설정을 "real" 프로필로 지정한 뒤, "dev" 프로필을 이용해서 스프링 컨테이너를 초기화할 수 있다. 그럼 "dev" 프로필에 정의된 빈을 사용하게 된다.
@configuration 어노테이션을 이용한 설정에서 프로필을 지정하려면 @Profile 어노테이션을 이용한다. @Configuration
@Profile("dev")
public class DsDevConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
}
@Configuration
@Profile("real")
public class DsRealConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
} 스프링 컨테이너를 초기화할 때 "dev" 프로필을 활성화하면 DsDevConfig 클래스를 설정으로 사용한다. 두 dataSource 빈 중에 어떤 빈을 사용할지는 활성화한 프로필에 따라 달라진다. 특정 프로필을 선택하려면 컨테이너를 초기화하기 전에 setActiveProfiles() 메서드를 사용해서 프로필을 설정해야 한다.
프로필을 사용할 때 주의할 점은 설정 정보를 전달하기 전에 어떤 프로필을 사용할지 지정해야 한다는 점이다. dev로 설정 후 register() 메서드로 설정 파일 목록을 지정한다. 이후 refresh() 메서드를 실행해서 컨테이너를 초기화했다. 시스템 프로퍼티를 이용하여 설정할 수도 있다.
자바의 시스템 프로퍼티뿐만 아니라 OS의 "spring.profiles.active" 환경 변수에 값을 설정해도 된다. 프로필의 우선순위는 다음과 같다.
중첩 클래스를 이용해서 프로필 설정을 한 곳으로 모을 수 있다. @Configuration
public class MemberConfigWithProfile{
@Autowired
private DataSource dataSource;
@Bean
public MemberDao memberDao(){
return new MemberDao(dataSource);
}
@Profile("dev")
public class DsDevConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
}
@Profile("real")
public class DsRealConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
}
}
스프링 설정은 두 개 이상의 프로필 이름을 가질 수 있다. @Configuration
@Profile("real,test")
public class DataSourceJndiConfig{
...
} 프로필을 설정할 때 느낌표를 사용하면 해당 프로필이 활성화되지 않았을 때 사용한다는 것을 의미한다. 보통 "!프로필" 형식은 특정 프로필이 사용되지 않을 때 기본으로 사용할 설정을 지정하는 용도로 사용된다. 2. 프로퍼티 파일을 이용한 프로퍼티 설정스프링은 외부의 프로퍼티 파일을 이용해서 스프링 빈을 설정하는 방법을 제공하고 있다. 다음과 같은 db.properties 파일이 있다고 하자. 이 파일의 프로퍼티 값을 자바 설정에서 사용할 수 있으며 이를 통해 설정 일부를 외부 프로퍼티 파일을 사용해서 변경할 수 있다.
자바 설정에서 프로퍼티 파일을 이용하려면 다음 두 가지를 설정한다.
public class PropertyConfig{
}
@value 어노테이션이 ${구분자} 형식의 플레이스홀더 값으로 갖고 있다. ${db.driver} 플레이스홀더 값을 db.properties에 "db.driver" 프로퍼티 값으로 치환한다. 따라서 실제 빈을 생성하는 메서드는 @value 어노테이션이 붙은 필드를 통해서 해당 프로퍼티의 값을 사용할 수 있다.
빈으로 사용할 클래스에도 @value 어노테이션을 붙일 수 있다. @value 어노테이션을 필드에 붙이면 플레이스홀더에 해당하는 프로퍼티를 필드에 할당한다. public class Info{
@Value("${info.version}")
private String version
...
} 다음과 같이 @value 어노테이션을 set 메서드에 적용할 수도 있다 public class Info{
private String version;
...
@Value("${info.version}")
public void setVersion(String version){
this.version=version;
}
} |
[Chapter 15] 간단한 웹 어플리케이션의 구조1. 웹 어플리케이션의 구성요소-프론트 서블릿 프론트 서블릿 : 웹 브라우저의 모든 요청을 받는 창구 역할을 하고 컨트롤러 : 웹 브라우저의 요청을 처리함, 요청을 처리하기위해 알맞을 기능을 실행하고 그 결과를 뷰에 전달한다.요청 처리를 위해 필요한 데이터는 서비스와 상호작용을 하며 얻게되며 컨트롤러의 주요 역할
*컨트롤러는 직접 비즈니스 로직을 구현하지 않고, 서비스에 처리를 위임한다. 서비스 : 기능의 로직을 구현함, 서비스는 애플리케이션의 핵심 기능을 담당하며, 데이터의 처리를 위해 DAO와 상호작용 한다. 이 말은 DB 연동이 필요하면 DAO를 사용한다 라고 설명할수있음 DAO : Data Access Object의 약자, DB와 웹 어플리케이션 간에 데이터를 이동시켜 주는 역할을 함, DAO를 통해 데이터베이스에 접근하여 데이터를 조회, 삽입, 업데이트, 삭제할 수 있다. 2. 서비스의 구현서비스 구현 예시 : 비밀번호 변경 기능 비밀번호 변경 기능의 로직은 다음과 같은 단계를 따른다.
이런 로직들은 한 번의 과정으로 끝나기 보다는 위의 예시 처럼 몇 단계의 과정을 거치곤 한다. 스프링에서는 @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: 메서드가 트랜잭션 내에서 실행됨을 나타냅니다. 트랜잭션 내에서 실행된 모든 작업이 성공하거나 실패하는 경우에만 데이터베이스에 반영됩니다. 서비스 메서드의 파라미터 전달 방식
public void changePAssword(String email, String oldPwd, String newPwd) 각 파라미터가 서비스 매서드에서 필요한 정보를 직접 전달한다.
public void regist(RegisterRequest req) 서비스 매서드에 필요한 데이터를 객체로 묶어 전달하는 방식이다.
@RequestMapping(method = RequestMethod.POST)
public String submit(
@ModelAttibute("command") ChangePwdCommand pwdCmd,
Errors errors, HttpSession session){
...
changePAsswordService.changePassword(
authInfo.getEmail(),
pwdCmd.getCurrentPassword(),
pwdCmd.getNewPassword();
...
} 스프링 MVC는 웹 요청 파라미터를 객체로 자동으로 바인딩해주는 기능을 제공한다. @ModelAttribute를 사용하면 폼 데이터가 자동으로 해당 객체의 필드에 바인딩됩니다. 장점: 서비스 메서드의 결과 전달 방식(2가지)
public class AuthService {
... 생략
public Authinfo authenticate(String email, String password) {
Member member = memberDao.selectByEmail(email);
if (member == null) {
throw new WrongldPasswordException);
}
if (!member.matchPassword(password)) {
throw new WrongldPasswordException();
}
return new AuthInfo(member.getld(), member.getEmail),
member.getName());
}
} 서비스 메서드가 정상적으로 수행되었을 경우, 메서드는 결과를 리턴합니다.
@RequestMapping(method = RequestMethod.POST)
public String submit(
LoginCommand loginCommand, Errors errors, HttpSession session,
HttpServletResponse response) {
...
try {
AuthInfo authInfo = authService.authenticate(
loginCommand.getEmail),
loginCommand.getPassword());
session.setAttribute("authinfo", authinfo);
...
return "login/loginSuccess";
} catch (WrongldPasswordException e) { <-예외처리
errors.reject("idPasswordNotMatching");
return "login/loginForm";
}
} 기능 실행 중 문제가 발생할 경우, 익셉션이 발생한다. 3. 컨트롤러에서의 DAO 접근서비스 메서드에서 로직 수행하지 않고 DAO 메서드만 호출하고 끝나기도 한다 public class MemberService{
```
public Member getMember(Long id){
return memberDao.selectById(id);
}
} getMember 메서드는 memberDao 객체의 selectById 메서드를 호출하여 데이터베이스에서 특정 id에 해당하는 Member 객체를 조회합니다.조회된 Member 객체를 그대로 반환합니다. 4. 패키지 구성패키지 구성 요소들은 웹 요청을 처리하기 위한 영역 기능 제공 영역에는 웹 어플리케이션이 복잡해진다면,, -> 도메인 주도 설계를 통해 해결한다. 컨트롤러-서비스-DAO 구조는 간단한 웹 어플리케이션을 개발할 때는 괜찮지만, 기능이 많아지고 로직이 추가되면 구조적인 부분의 코드도 함께 복잡해진다. 도메인 주도 설계를 통해 해결할수있다. 중요한 것은 도메인 모델 및 업무 로직이 서비스 영역이 아닌 도메인 영역에 위치한다는 것이다.도메인 영역은 정해진 패턴에 따라 모델을 구현하므로, 업무가 복잡해져도 일정 수준의 복잡도로 코드를 유지할 수 있도록 해준다. [Chapter 16] JSON 응답과 요청1. JSON 개요SON (JavaScript Object Notation)은 데이터를 구조화하고 교환하는 데 널리 사용되는 경량의 데이터 형식입니다. JSON은 사람과 기계가 모두 쉽게 읽고 쓸 수 있으며, 클라이언트와 서버간의 데이터 교환에 주로 사용된다. {
"name":"유관순",
"birthday": "1902-12-16",
"age":"17",
"related":["남동순","류예도"]
```
} JSON의 기본 구조 객체(Object): {}로 둘러싸인 키-값 쌍의 집합입니다. 키는 문자열로, 값은 다양한 데이터 타입이 될 수 있습니다. 배열(Array): []로 둘러싸인 값의 순서 있는 집합입니다. 배열의 요소는 객체, 배열, 문자열, 숫자 등 다양한 데이터 타입이 될 수 있습니다. 위의 예시는 유관순이라는 사람에 대한 정보를 담고있습니다. 각 키-값 쌍이 다음과 같은 의미를 갖게된다. "name": "유관순": 이름이 유관순인 사람 JSON의 특징
이러한 특징 때문에 JSON은 웹 애플리케이션의 데이터 교환 표준으로 널리 사용되고 있습니다. 2. Jackson 의존 설정Jackson - 자바 객체와 JSON 간의 변환을 처리하는 라이브러리 스프링에서는 Jackson을 이용하여 자바 객체를 JSON 형식으로 직렬화하거나, JSON 데이터를 자바 객체로 역직렬화하는 작업을 쉽게 할 수 있습니다. 스프링에서 Jackson을 사용하려면, pom.xml 파일에 Jackson 관련 의존성을 추가하면 된다. 자바 객체와 JSON간의 변환 예시. public class Person{
private String name;
private int age;
...get/set 메서드
} ^
Person 객체를 JSON 형식으로 변환하면 다음과 같은 JSON 문자열이 생성됩니다 3. @RestController로 JSON 형식 응답스프링에서 JSON 형식으로 데이터를 응답하는 방법 @RestController 사용법 @RestController는 @controller와 @responsebody를 결합한 형태로, 메서드의 반환값을 HTTP 응답 본문으로 직접 전송합니다. 따라서, 별도로 @responsebody를 사용할 필요가 없습니다. 3.1 @JsonIgnore를 이용한 제외 처리@JsonIgnore 애노테이션을 사용하면 JSON 직렬화 및 역직렬화 시 특정 필드를 무시할 수 있습니다. 이는 특히 암호와 같은 민감한 정보를 API 응답에 포함시키지 않기 위해 유용합니다. @JsonIgnore를 적용한 필드는 JSON 변환 과정에서 무시되어, 클라이언트에게 전송되지 않습니다. 다음과 같이 JSON 응답에 포함시키지 않을 대상에 @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 registerDateTime;
} 사용 시 주의점 3.2 날짜 형식 변환 처리 : @jsonformat 사용@jsonformat 애노테이션을 사용하면 Jackson을 통해 변환 할때 날짜와 시간의 형식을 지정할 수 있습니다. 기본적으로 Jackson은 LocalDateTime을 ISO-8601 형식으로 출력하지만, 이를 원하는 형식으로 조정하고자 할 때 @jsonformat의 pattern 속성을 활용할 수 있습니다. 기본 ISO-8601 형식 출력 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;
} 해당 부분 출력 결과 : "2018-03-01T11:07:49" 사용자 지정 패턴 사용 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(pattern = "yyyyMMddHHmmss")을 사용 3.3 날짜 형식 변환 처리 : 기본 적용 설정날짜 형식을 변환할 모든 대상에 @jsonformat을 붙여야 한다면 번거로운데 ...생략
public class MvcConfig implements webMvcConfigurer{
...생략
@Override
public void extendMessageConverters(
List<HttpMessageConverter<?>> converters){
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
*.featuresToDisable(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)*
//타임스탬프형식 비활성화
.build();
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
} ObjectMapper를 생성하고, SerializationFeature.WRITE_DATES_AS_TIMESTAMPS 기능을 비활성화하여 날짜가 타임스탬프가 아닌 ISO-8601 형식으로 출력되도록 합니다. 4. @RequestBody로 JSON 요청 처리지금까지는 응답을 JSON으로 변환하는거였고, POST 방식이나 PUT 방식을 사용하면 name=이름&age=17 과 같은쿼리 문자열 형식이 아니라 다음과같은 형식의 데이터를 요청 데이터로 전송 할 수 있다.
방법 -> 커맨드 객체에 @RequestBody만 붙이면 됨 ...생략
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
...생략
@RestController
public class RestMemberController{
...
@PostMapping("/api/members")
public void newMember(
**@RequestBody** @valid RegisterRequest regReq,
HttpServletResponse response) throws IOException{
...
}
}
} @RequestBody는 클라이언트가 전송한 JSON 형식의 요청 본문을 자바 객체로 자동으로 변환합니다. 이 과정에서 스프링의 메시지 변환기(HttpMessageConverter)가 Jackson을 사용하여 JSON을 자바 객체로 변환합니다. 4.1 JSON 데이터의 날짜 형식 다루기yyyy-MM-ddTHH:mm:ss와 같이 특정 패턴 문자열을 LocalDateTime과 Date로 변환하고 싶다면 @JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime birthDateTime;
@JsonFormat(pattern = "yyyyMMdd HHmmss")
private Date birthDate; 특정 속성이 아니라 해당 타입을 갖는 모든 속성에 적용하고 싶다면 스프링 MVC 설정을 추가하면 된다. ...
public class MvcConfig implements VecMvcConfigurer{
...
@Override
public void extendMessageConverters(
List<HttpMessageConverter<?>> converters){
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
.featuresToDisable(SerializationFeature.INDENT_OUTPUT)
** .deserailizerByType(LocalDateTime.class,
new LocalDateTimeeserializer(formatter))
.simpleDateFormat("yyyyMMdd HHmmss") **
.build();
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
} 4.2 요청 객체 검증하기JSON 요청 데이터를 자바 객체로 변환한 후, 데이터의 유효성을 검증하는 것은 매우 중요합니다. 이를 통해 잘못된 데이터 입력을 방지할 수 있습니다. 요청 객체의 검증을 위해 @Valid 애노테이션과 Validator를 사용할 수 있다. @Valid를 사용한 경우 검증에 실패하면 400 상태코드를 응답한다. Validator를 사용할 경우 다음과 같이 직접 상태 코드를 처리해야 한다. PostMapping("/api/members")
public void newMember(
@RequestBody RegisterRequest regRea, Errors errors,
HttpServletResponse response) throws IOException {
try {
// Validator를 통해 수동 검증
new RegisterRequestValidator) validate(regRea, errors);
// 검증 실패 시 400 Bad Request 응답
if (errors.hasErrors()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
} ...
} catch (DuplicateMemberException dupEx) {
response.sendError(HttpServletResponse.SC_CONFLICT);
}
} 5. ResponseEntity로 객체 리턴하고 응답 코드 지정하기ResponseEntity를 사용하면 API 응답을 보다 유연하게 제어할 수 있으며, JSON 응답과 상태 코드를 함께 설정할 수 있습니다. 5.1 ResponseEntity를 이용한 응답 데이터 처리정상인 경우와 비정상인 경우 둘 다 JSON 응답 전송하는 방법은 ResponseEntity를 사용하는 것이다. import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@RestController
public class RestMemberController {
private MemberDao memberDao;
.. 생략
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member @PathVariable Long id) {
Member member = memberDao.selectByld (id);
if (member == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
return ResponseEntity.status(HttpStatus.OK).body(member);
} 리턴타입이 ResponseEntity면 ResponseEntity의 body로 지정한 객체를 사용해 변환한다 상태코드와 몸체 데이터를 설정하는 기본 방법
상태코드 200(OK) 와 몸체데이터를 설정하는 방법
몸체 내용이 없을때 body를 지정하지 않고 build()로 바로 생성
또는 status() 메서드 대신에 다음과 같이 관련 메서드를 사용해도 된다. (몸체내용x)
몸체가 없을때 status() 대신 사용할수있는 메서드 5.2 @ExceptionHandler 적용 메서드에서 ResponseEntity로 응답하기한 메서드에서 정상 응답과 에러 응답을 ResponseBody로 생성하면 코드가 중복될 수 있다. @ExceptionHandler를 사용하면 컨트롤러의 메서드에서 발생한 예외를 처리하고, 공통된 에러 응답 로직을 중앙에서 관리할 수 있다. 이 접근법을 사용하면 중복된 코드 작성 없이 에러 응답을 일관되게 처리할 수 있다. @ExceptionHandler를 이용한 에러 응답 처리 -> 중복없애짐 @GetMapping("/api/members/{id}")
public ResponseEntity<Object> member @PathVariable Long id) {
Member member = memberDao.selectByld (id);
if (member == null) {
throw new MemberNotFoundException();
}
return member;
}
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
} getMember 메서드는 정상적인 경우 Member 객체를 JSON으로 변환하여 응답합니다. 5.3 @Valid 에러 결과를 JSON으로 응답하기@Valid를 붙인 커맨드 객체가 값 검증에 실패하면 400 상태코드를 응답한다. 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이 발생한다. [Chapter 17] 프로필과 프로퍼티 파일1. 프로필개발을 진행하는 동안에는 실제 서비스 목적으로 운영중인 DB를 이용할 수 없음 실제 서비스 환경에서는 웹 서버와 DB서버가 서로 다른 장비에 설치된 경우가 많음 실 서비스 장비에 배포하기 전에 설정 정보를 변경하고 배포하는 방법은 원시적이며 실수하기 쉽다. 이러한 실수를 방지하기 위한 방법은?? 설정 집합에 프로필을 지정할 수 있으며 스프링 컨테이너는 설정 집합 중에서 지정한 이름을 사용하는 프로필을 선택하고 해당 프로필에 속한 설정을 이용해서 컨테이너를 초기화할 수 있음 1.1 @configuration 설정에서 프로필 사용하기@configuration 어노테이션을 이용한 설정에서 프로필을 지정하려면 @Profile 애노테이션을 이용한다. @Configuration
@Profile("dev")
public class DsDevConfig{
} @Configuration
@Profile("real")
public class DsRealConfig{
} 같은 타입의 빈을 설정하고 있다고 가정하고 특정 프로필을 선택하려면 컨테이너를 초기화하기 전에 setActiveProfile() 메서드를 사용해서 프로필을 선택해야함. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.getEnvrionment().setActiveProfiles("dev");
//-> "dev" 프로필에 속한 설정이 사용되는 것
context.register(MemberConfig.class, DsDevConfig.class, DsRealConfig.class);
context.refresh(); 프로필을 사용할 때 주의할 점
이 순서를 지키지 않고 프로필을 선택하기 전에 설정 정보를 먼저 전달하면 프로필을 지정한 설정이 사용되지 않기 때문에 설정을 읽어오는 과정에서 빈을 찾지 못해 익셉션이 발생함. 두개 이상 프로필을 활성화 하고 싶다면?? context.getEnvironment().setActiveProfiles("dev","mysql") 프로필을 선택하는 또다른 방법 방법 2 시스템 프로퍼티는 명령행에서 -D 옵션을 이용하거나 System.setProperty()를 이용해서 지정할 수 있음
위와 같이 시스템 프로퍼티로 프로필을 설정하면 setActivePropfiles() 메서드를 사용하지않아도 "dev" 프로필이 활성화된다. 방법 3 프로필 우선순위 1.2 @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{
```
}
} 주의할 점 : 중접 클래스는 static이어야 한다. 1.3 다수 프로필 설정스프링 설정은 두 개 이상의 프로필 이름을 가질 수 있다. @Configuration
@Profilde("real,test")
public class DataSourceJndiConfig{
} 프로필 값을 지정할때 느낌표(!)를 사용할 수도 있다. @Configuration
@Profile("!real")
public class DsDevConfig{
```
} !프로필 형식은 특정 프로필이 사용되지 않을 때 기본으로 사용할 설정을 지정하는 용도로 사용된다. 1.4 어플리케이션에서 프로필 설정하기웹 어플리케이션의 경우에 프로필 선택 방법
2. 프로퍼티 파일을 이용한 프로퍼티 설정스프링은 외부의 프로퍼티 파일을 이용해서 스프링 빈을 설정하는 방법을 제공하고 있다.
이 파일의 프로퍼티 값을 자바 설정에서 사용할 수 있으며 이를 통해 설정 일부를 외부 프로퍼티 파일을 사용해서 변경할 수 있다. 2.1 @configuration을 이용 자바 설정에서의 프로퍼티 사용자바 설정에서 프로퍼티를 사용하려면 다음 두가지를 설정한다
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySoucesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
@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;
}
} setLocations() 메서드는 프로퍼티 파일 목록을 인자로 전달받는다 주의할점! PropertySourcesPlaceholderConfigurer 타입 빈은 setLocatuons() 메서드로 전달받은 프로퍼티 파일 목록 정보를 읽어와서 필요할 때 사용한다. package config;
```
@Congifuration
public class DsConfigWithProp{
@Value("${db.driver}")
private String driver;
```
} @value 애노테이션이 ${구분자} 형식의 플레이스홀더를 값으로 갖고있다 위 예의 경우 ${db.driver} 플레이스홀더를 db.properties에 정의되어있는 "db.driver" 프로퍼티 값으로 치환한다. 따라서 실제 빈을 생성하는 메서드는 @value 애노테이션이 붙은 필드를 통해서 해당 프로퍼티의 값을 사용할 수 있다. 2.2 빈 클래스에서 사용하기빈으로 사용할 클래스에도 @value 애노테이션을 붙일 수 있다. package spring;
import org.springframework.beans.factory.annotation.Value;
public class Info{
@Value("${Info.version}")
private String version;
```
} 다음과 같이 @value 애노테이션을 set 메서드에 적용할 수도 있다 public class Info{
private String version;
```
@Value("${info.version}")
public void setVersion(String version){
this.version=version;
}
} |
스프링 MVC 프레임워크 동작 방식스프링 MVC 핵심 구성 요소지금부터 스프링 MVC의 핵심 구성 요소와 각 요소 간의 관계를 알아보고자 한다. 컨트롤러 객체를 DispatcherServlet이 전달받았다고 해서 바로 컨트롤러 객체의 메서드를 실행할 수 있는 것은 아니다. 스프링 MVC는 다양한 방식의 핸들러(컨트롤러)를 지원하므로, 각 핸들러를 유연하게 처리하기 위해 적절한 HandlerAdapter를 사용해야한다.
이는 @controller 애노테이션을 붙인 클래스를 이용해서 클라이언트의 요청을 처리하지만 자신이 직접 만든 클래스를 이용해 클라이언트의 요청을 처리할 수도 있기 때문이다. DispatcherServlet과 스프링 컨테이너
JSP를 위한 ViewResolver
컨트롤러의 실행 결과를 받은 DispatcherServlet은 ViewResolver에게 뷰 이름에 해당하는 View 객체를 요청한다.
DispatcherServlet은 컨트롤러의 실행 결과를 HandlerAdapter를 통해서 ModelAndView형대로 받는다고 했다. 디폴트 핸들러와 HandlerMapping의 우선순위디폴트 핸들러는 어떠한 HandlerMapping에도 매핑되지 않는 요청을 처리하는 핸들러이다. 이를 통해 특정 URL 패턴을 처리하지 않는 나머지 모든 요청을 처리할 수 있다.
DispatcherServlet은 웹 브라우저의 요청을 받기 위한 창구 역할을 하고, 다른 주요 구성 요소들을 이용해서 요청 흐름을 제어하는 역할을 한다. |
##MVC1 : 요청 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델 요청 매핑웹 어플리케이션을 개발하는 것은 다음 코드를 작성하는 것이다.
이 중 첫 번째는 @controller 애노테이션을 사용한 컨트롤러 클래스를 이용해서 구현한다.
컨트롤러 클래스는 요청 매핑 애노테이션(@RequestMapping, @GetMapping 등) 을 사용해서 메서드가 처리할 요청 경로를 지정한다. 요청 매핑 애노테이션을 적용한 메서드를 두 개 이상 정의하는 것도 가능하다. Get과 Post@RequestMapping은 HTTP 메서드(GET, POST, PUT, DELETE 등)를 포함하여 다양한 요청을 매핑하는 데 사용할 수 있는 다목적 어노테이션이다.
Get, Post 애노테이션을 사용하면 위의 코드처럼 같은 경로에 대해 Get과 Post 방식을 각각 다른 메서드가 처리하도록 설정할 수 있다. 요청 파라미터 접근컨트롤러 메서드에서 요청 파라미터를 사용하는 방법은 책에서 두가지가 나와있다.
첫 번째 방법은 위의 코드처럼 컨트롤러 처리 메서드의 파라미터로 HttpServletRequest 타입을 사용하고 getParameter() 메서드를 이용해서 파라미터의 값을 구하는 것이다.
요청 파라미터에 접근하는 두 번째 방법은 위의 코드처럼 @RequestParam 애노테이션을 사용하는 것이다. 위의 handleStep1 메서드는 agree 요청 파라미터 값이 true가 아니라면 "haewon/step1" 뷰 이름을 리턴할 것이고 화면은 왼쪽 그림처럼 뜨게 될 것이다. true라면 "haewon/step2" 뷰 이름을 리턴할 것이고 화면은 오른쪽그림 처럼 뜨게 될것이다. 리다이렉트 처리웹 브라우저에서 http:/localhost:8080/~~~/haewon/step1 주소를 직접 입력하면 에러 화면이 출력된다. 하지만 잘못된 전송 방식으로 요청이 왔을 때 에러 화면보다 알맞은 경로로 리다이렉트 하는 것이 더 좋을 수 있다. 컨트롤러에서 특정 페이지로 리다이렉트 시키는 방법은 "redirect:경로"를 뷰 이름으로 리턴하면 된다
위 코드는 /haewon/step1 경로를 Get방식으로 접근할 때 /haewon/step2 경로로 리다이렉트 해준 코드이다. 커맨드 객체를 이용해 요청 파라미터 사용하기step1.jsp가 생성하는 폼이 email, name, password, confirmPassword 파라미터를 이용해서 정보를 서버에 전송한다고 해보자. 폼 요청을 처리하는 컨트롤러 코드는 각 파라미터의 값을 구하기 위해 다음과 같은 코드를 사용할 수 있다.
위 코드는 올바르게 동작하나, 요청 파라미터 개수가 증가할 때마다 코드의 길이도 길어지게된다.
커맨드 객체는 다음과 같이 요청 매핑 애노테이션이 적용된 메서드의 파라미터에 위치한다. 뷰 JSP 코드에서 커맨드 사용하기스프링 MVC는 커맨드 객체의 (첫 글자를 소문자로 바꾼) 클래스 이름과 동일한 속성 이름을 사용해서 커맨드 객체를 뷰에 전달한다.
위 코드는 @ModelAttribute를 사용하여 뷰 코드에서 formData라는 이름으로 커맨드 객체에 접근할 수 있게 되었다. 주요 에러 발생 상황요청 매핑 애노테이션과 관련된 주요 익셉션
@RequestParam이나 커맨드 객체와 관련된 주요 익셉션
Model을 통해 컨트롤러에서 뷰에 데이터 전달하기컨트롤러는 뷰가 응답 화면을 구성하는데 필요한 데이터를 생성해서 전달해야하는데 이때 사용하는 것이 Model이다.
addAttribute()메서드의 첫번째 파라미터는 속성 이름이다. 뷰 코드는 이 이름을 사용해서 데이터에 접근한다.
|
1 JDBC 프로그래밍의 단점을 보완하는 스프링
→ 그냥 구조적으로 반복되고 중복되는 코드가 많음. → 그래서 스프링은 구조적인 반복을 줄이기 위해 템플릿 메서드 패턴 과 전략 패턴 을 함께 사용하기 위해 JdbcTemplate 클래스를 제공함. 그리고 스프링의 또 다른 장점은 트랜잭션 관리가 쉽다는 것임. JDBC API로 트랜잭션을 처리하려면 Connnection의 setAutoCommit(false)을 이용해서 자동 커밋을 비활성화하고, commit()과 rollback() 메서드를 이용해서 트랜잭션을 커밋하거나 롤백해야함 또한 트랜잭션을 적용하고 싶은 메서드에 @transactional 애노테이션을 붙이기만 하면 커밋과 롤백처리는 스프링이 알아서 처리함. 실제 서비스 운영 환경에서는 서로 다른 장비를 이용해서 자바 프로그램과 DBMS를 실행함. 자바 프로그램에서 DBMS로 커넥션을 생성하는 시간은 매우 길기 때문에 DB 커넥션을 생성하는 시간은 전체 성능에 영향을 줄 수 있음. → 최초 연결에 따른 응답 속도 저하와 동시 접속자가 많을 때 발생하는 부하를 줄이기 위해 사용하는 것이 커넥션 풀 이다.
2 DB 테이블 생성이 장에서는 DB를 사용해서 MemberDao 클래스를 구현함 package spring;
import java.util.Collection;
public class MemberDao{
public Member selectByEmail(String email){
return null;
}
public void insert(Member member){
}
public void update(Member member){
}
public Collection<Member> selectAll(){
return null;
}
} 그리고 DBMS로 MySQL을 사용함.
3 DataSource 설정JDBC API는 DriverManager 외에 DataSource를 이용해서 DB연결을 구함 DataSource를 사용하면 다음 방식으로 Connection을 구할 수 있음 Connection conn = null;
try{
//dataSource는 생성자나 설정 메서드를 이용해서 주입받음
conn = dataSource.getConnection();
... → DB 연동에 사용할 DataSource를 스프링 빈으로 등록하고, DB 연동 기능을 구현한 빈 객체는 DataSource를 주입받아 사용함 ( Tomcat JDBC 모듈은 javax.sql.DataSource를 구현한 DataSource 클래스를 제공) 이 클래스를 스프링 빈으로 등록해서 DataSource로 사용할 수 있다 package config;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DbConfig {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
ds.setUsername("spring5");
ds.setPassword("spring5");
ds.setInitialSize(2);
ds.setMaxActive(10);
return ds;
}
}
@bean(destroyMethod = "close") 이 destroyMethod 속성값을 close로 설정하여 커넥션 풀에 보관된 Connection을 닫음 3.1 Tomcat JDBC의 주요 프로퍼티Tomcat JDBC 모듈의 org.apache.tomcat.jdbc.pool.DataSource 클래스는 커넥션 풀 기능을 제공하는 DataSource 구현 클래스다. 이때 커넥션의 상태를 알아야하는데, 커넥션 풀은 커넥션을 생성하고 유지함. 커넥션 풀에 커넥션을 요청하면 해당 커넥션은 활성(active) 상태가 되고, 커넥션을 다시 커넥션 풀에 반환하면 유휴(idle) 상태가 됨. →DataSource#getConnection()을 실행하면 커넥션이 활성상태가 되고, close하면 커넥션은 풀로 돌아가 유휴 상태가 됨. package dbquery;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
public class DbQuery {
private DataSource dataSource;
public DbQuery(DataSource dataSource) {
this.dataSource = dataSource;
}
public int count() {
Connection conn = null;
try {
conn = dataSource.getConnection();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select count(*) from MEMBER")) {
rs.next();
return rs.getInt(1);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (conn != null)
try {
conn.close();
} catch (SQLException e) {
}
}
}
} 이때, conn = dataSource.getConnection(); 를 실행해서 DataSource에서 커넥션을 가져와 커넥션 conn 은 활성상태가 되고, conn.close(); 을 실행하여 커넥션을 종료하면 실제 커넥션을 끊지 않고 풀에 반환함. 그럼 conn은 유휴 상태가 됨.
커넥션 풀에 생성된 커넥션은 지속적으로 재사용되며, 한 커넥션이 영원히 유지되는 것은 아님 → ex) DBMS에 5분동안 쿼리를 실행하지 않으면 DB 연결을 끊도록 설정했는데 DBMS는 해당 커넥션의 연결을 끊지만 커넥션은 여전히 풀 속에 남아있음. 이때 해당 커넥션을 풀에서 가져와 사용하면 익셉션이 발생하게 됨. → 이런 문제를 방지하기 위해 커넥션 풀의 커넥션이 유효한지 주기적으로 검사해야됨. 이와 관련된 속성이 minEvictableIdleTimeMillis, timeBetweenEvictionRunsMillis, testWhileIdele 임. ds.setTestWhileIdle(true); //유휴 커넥션 검사
ds.setMinEvictableIdleTimeMillis(60000 * 3); //최소 유휴 시간 3분
ds.setTimeBetweenEvictionRunsMillis(10 * 1000); //10초 주기 4 JdbcTemplate을 이용한 쿼리 실행스프링을 사용하면 JdbcTemplate을 이용해서 편리하게 쿼리를 실행할 수 있음 MemberDao클래스에 코드를 채워나가 보자 가장 먼저 JdbcTemplate 객체를 생성 package spring;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class MemberDao{
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource){
**this.jdbcTemplate = new JdbcTemplate(dataSource);**
}
} JdbcTemplate 객체를 생성하려면 this.jdbcTemplate = new JdbcTemplate(dataSource); 처럼 DataSource를 생성자게 전달하면 됨. JdbcTemplate을 생성하는 코드를 MemberDao 클래스에 추가했으니 스프링 설정에 MemberDao 빈 설정을 추가하자 @Configuration
public class AppCtx{
@Bean(destroyMethod = "close")
public DataSource dataSource(){
DataSource ds = new DataSource();
...
}
**@Bean
public MemberDao memberDao(){
return new MemberDao(dataSource());
}**
} JdbcTemplate 클래스는 SELECT 쿼리 실행을 위한 query() 메서드를 제공함 query() 메서드는 sql 파라미터로 전달받은 쿼리를 실행하고 RowMapper를 이용해서 ResultSet의 결과를 자바 객체로 변환 select * from member where email = ? 이런 인덱스 기반 파라미터를 가진 쿼리이면 args 파라미터를 이용해서 각 인덱스 파라미터의 값을 지정함 RowMapper의 mapRow() 메서드는 SQL 실행 결과로 구한 ResultSet에서 한 행의 데이터를 읽어와 자바 객체로 변환하는 매퍼 기능을 구현함 package spring;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Member selectByEmail(String email) {
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("REGDATE").toLocalDateTime());**
member.setId(rs.getLong("ID"));
return member;
}
}, **email**);
return results.isEmpty() ? null : results.get(0);
} "select * from MEMBER where EMAIL = ?" 은 jdbcTemplate의 query() 메서드를 이용해 쿼리를 실행함. 이 쿼리는 인덱스 파라미터(물음표)를 포함함. 그리고 이 인덱스 파라미터에 들어갈 값을 email 로 지정함. 그리고 new RowMapper 이 부분부터 임의 클래스를 이용해서 RowMapper의 객체를 전달함. 이 RowMapper는 ResultSet에서 데이터를 읽어와 Member객체로 변환해주는 기능을 제공함
MemberDao에서 jdbcTemplate의 query()를 사용하는 또 다른 메서드는 selectAll()이다. 참고로 selectAll() 메서드의 리턴타입을 Collection에서 List로 변경함 package spring;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Member selectByEmail(String email) {
..
}
public List<Member> selectAll() {
List<Member> results = jdbcTemplate.query("select * from MEMBER",
(ResultSet rs, int rowNum) -> {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
});
return results;
} 위 코드는 setByEmail() 메서드와 동일한 RowMapper 임의 클래스를 사용함. 이와같이 Member를 위한 RowMapper 구현 클래스를 이용하도록 두 메서드를 수정하면 RowMapper 임의 클래스나 람다 식 중복을 제거할 수 있다. 그리고 Member 테이블의 전체 행 개수를 구하는 코드를 추가해보자. 이 코드도 query() 메서드를 사용했다. public int count() {
Integer count = jdbcTemplate.queryForObject(
"select count(*) from MEMBER", Integer.class);
return count;
} count(8) 쿼리는 결과가 한 행뿐이니 쿼리 결과를 List로 받기보다는 Integer와 같은 정수타입으로 받으면 편리하다. 이를 위한 메서드가 queryForObject()이다. 이 메서드는 쿼리 실행 결과 행이 한 개인 경우에 사용할 수 있다. 두번째 파라미터인 Integer.class는 칼럼을 읽어올 때 사용할 타입을 지정하는 것이다. ex) 평균을 구한다면 Double.class를 사용할 수 있다. INSERT,UPDATE,DELETE 쿼리는 update() 메서드를 사용한다. public void update(Member member) {
jdbcTemplate.update(
"update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
member.getName(), member.getPassword(), member.getEmail());
} update() 메서드는 쿼리 실행 결과로 변경된 행의 개수를 리턴한다. MySQL의 AUTO_INCREMENT 칼럼은 행이 추가되면 자동으로 값이 할당되는 칼럼으로서 주요키 칼럼에 사용된다. 따라서 자동 증가 칼럼에 해당하는 값은 지정하지 않는다. 그런데 쿼리 실행 후에 변경된 키값을 알고싶다면 어떻게 해야할까? JdbcTemplate는 자동으로 생성된 키 값을 구할 수 있는 방법을 제공하는데, 그게 바로 KeyHolder를 사용하는 것이다. KeyHolder를 사용하면 MemberDao의 insert() 메서드에서 삽입하는 Member 객체의 ID값을 구할 수 있다. public void insert(Member member) {
**KeyHolder keyHolder = new GeneratedKeyHolder();**
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con)
throws SQLException {
// 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 생성
PreparedStatement pstmt = con.prepareStatement(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
"values (?, ?, ?, ?)",
new String[] { "ID" });
// 인덱스 파라미터 값 설정
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getName());
pstmt.setTimestamp(4,
Timestamp.valueOf(member.getRegisterDateTime()));
// 생성한 PreparedStatement 객체 리턴
return pstmt;
}
}, keyHolder);
Number keyValue = keyHolder.getKey();
member.setId(keyValue.longValue());
}
즉, JdbcTemplate의 update() 메서드는 PreparedStatement를 실행한 후 자동 생성된 키값을 KeyHolder에 보관하고, 이 보관된 키값은 getKey() 메서드를 이용해서 구한다. 5 MemberDao 테스트하기지금까지 JdbcTemplate을 이용해서 MemberDao 클래스를 완성했다. 이 설정을 사용하는 메인 클래스를 통해 MemberDao가 정상적으로 동작하는지 보자 package main;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import config.AppCtx;
import spring.Member;
import spring.MemberDao;
public class MainForMemberDao {
private static MemberDao memberDao;
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppCtx.class);
memberDao = ctx.getBean(MemberDao.class);
selectAll();
updateMember();
insertMember();
ctx.close();
}
private static void selectAll() {
System.out.println("----- selectAll");
int total = memberDao.count();
System.out.println("전체 데이터: " + total);
List<Member> members = memberDao.selectAll();
for (Member m : members) {
System.out.println(m.getId() + ":" + m.getEmail() + ":" + m.getName());
}
}
private static void updateMember() {
System.out.println("----- updateMember");
Member member = memberDao.selectByEmail("[email protected]");
String oldPw = member.getPassword();
String newPw = Double.toHexString(Math.random());
member.changePassword(oldPw, newPw);
memberDao.update(member);
System.out.println("암호 변경: " + oldPw + " > " + newPw);
}
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("MMddHHmmss");
private static void insertMember() {
System.out.println("----- insertMember");
String prefix = formatter.format(LocalDateTime.now());
Member member = new Member(prefix + "@test.com",
prefix, prefix, LocalDateTime.now());
memberDao.insert(member);
System.out.println(member.getId() + " 데이터 추가");
}
}
MainForMemberDao 클래스를 실행할 때 DB 연결 정보가 올바르지 않으면 다음과 같은 익셉션이 발생할 수 있다. (콘솔에 메시지가 출력됨)
이 에러메시지는 MySQL 서버에 연결할 권한이 없는 경우에 발생한다. 예를 들어 MySQL DB에 접근할 때 사용한 ‘spring5’ 계정의 암호를 잘못 입력한 경우 위 메시지가 발생한다.
이건 DB를 실행하지 않았거나 방화벽에 막혀 있어서 DB에 연결할 수 없다면 연결 자체를 할 수 없다는 에러 메시지가 출력된다 6 스프링의 익셉션 변환 처리SQL 문법이 잘못됐을 때 발생한 메시지를 보면 익셉션 클래스가 org.springframework.jdbc 패키지에 속한 BadSqlGrammarException 클래스임을 알 수 있다. JDBC API를 사용하는 과정에서 SQLException이 발생하면 이 익셉션을 알맞은 DataAccessException으로 변환해서 발생한다.
주된 이유는 연동 기술에 상관없이 동일하게 익셉션을 처리할 수 있도록 하기 위함이다. 그래서 각 연동 기술에 따라 발생하는 익셉션을 스프링이 제공하는 익셉션으로 변환함으로써 구현 기술에 상관없이 동일한 코드로 익셉션을 처리할 수 있게 된다. 7 트랜잭션 처리만약 이메일 인증 시점에 테이블의 데이터를 변경하는 기능은 다음 코드처럼 회원 정보에서 이메일을 수정하고, 인증 상태를 변경하는 두 쿼리를 실행할 것이다. jdbcTemplate.update("update MEMBER set EMAIL = ?", email);
jdbcTemplate.update("insert into EMAIL_AUTH values (?,'T')", email); 그런데 만약 첫번째 쿼리를 실행한 후 두번째 쿼리를 실행하는 시점에서 문제가 발생하면 어떻게 될까? → 결과적으로는 두번째 쿼리 실행에 실패하면 첫번째 쿼리 실행 결과도 취소해야 올바른 상태를 유지한다.
JDBC는 Connection의 setAutoCommit(false)를 이용해서 트랜잭션을 시작하고, commit()과 rollback()을 이용해서 트랜잭션을 반영하거나 취소한다. 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 애노테이션만 붙이면 된다. → 스프링은 @transactional 애노테이션이 붙은 changePassword() 메서드를 동일한 트랜잭션 범위에서 실행한다. 이 애노테이션이 제대로 동작하려면 다음의 두가지 내용을 스프링 설정에 추가해야한다.
트랜잭션 처리를 위한 설정을 완료하면 이제 @transactional 애노테이션을 붙이면 된다. 근데 트랜잭션을 시작하고, 커밋하고, 롤백하는 것은 누가 어떻게 처리하는 걸까? → 앞서 7장에서 공통으로 적용되는 기능을 구현하는 방법으로 AOP를 설명했는데, 트랜잭션도 공통 기능 중 하나이다. 스프링은 @transactional 애노테이션을 이용해서 트랜잭션을 처리하기 위해 내부적으로 AOP를 사용한다. 그러므로 트랜잭션 처리도 프록시를 통해서 이루어진다는 것을 알 수 있다. 실제로 @transactional 애노테이션을 적용하기 위해 @EnableTransaction Management 태그를 사용하면 스프링은 @transactional 애노테이션이 적용된 빈 객체를 찾아서 알맞은 프록시 객체를 생성한다 → 프록시 객체는 @transactional 애노테이션이 붙은 메서드를 호출하면 PlatformTransactionManager를 사용해서 트랜잭션을 시작하고, 실제 객체의 메서드를 호출한 뒤 성공적으로 실행되면 트랜잭션을 커밋한다.
try{
cps.changePassword("[email protected]", "1234", "1111");
System.out.println("암호를 변경했습니다.");
} catch (MemberNotFoundException e){
System.out.println("회원 데이터가 존재하지 않습니다.");
} catch (WrongPasswordException e){
System.out.println("암호가 올바르지 않습니다.");
} 이 코드의 실행결과를 보면 WrongPasswordException이 발생했을 때 트랜잭션이 롤백된 것을 알 수 있다. 실제로 @transactional을 처리하기 위한 프록시 객체는 원본 객체의 메서드를 실행하는 과정에서 RuntimeException이 발생하면 트랜잭션을 롤백한다
|
8장 |
스프링 프로그래밍 입문 5 남은 부분 정독
The text was updated successfully, but these errors were encountered: