-
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주차] 수빈, 지현, 예원 #6
Comments
15장 간단한 웹 어플리케이션의 구조간단한 웹 어플리케이션의 구성 요소간단한 웹 어플리케이션을 개발할 때 사용하는 전형적인 구조
프론트 서블릿
컨트롤러
컨트롤러는 어플리케이션이 제공하는 기능과 사용자 요청을 연결하는 매개체로, 기능 제공을 위한 로직을 직접 수행하지는 않는다. 대신, 해당 로직을 제공하는 서비스에 그 처리를 위임한다. @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에 비밀번호 변경 처리를 위임한 것을 알 수 있다. 서비스
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 구조는 간단한 웹 어플리케이션을 개발할 때는 괜찮지만, 기능이 많아지고 로직이 추가되면 구조적인 부분의 코드도 함께 복잡해진다. 도메인 주도 설계의 적용 위에서 발생한 문제를 해결하기 위한 방법 중 하나이다. 16장 JSON 응답과 요청 처리JSON 개요JSON(JavaScript Object Notation)은 간단한 형식을 갖는 문자열로, 데이터 교환에 주로 사용된다. JSON 형식으로 표현한 데이터 {
"name":"유관순",
"age":"17",
"related":["남동순","류예도"],
...
} 중괄호를 사용해 객체를 표현하며, 객체는 콜론으로 구분된 (이름, 값) 쌍을 갖는다. Jackson 의존 설정Jackson은 자바 객체와 JSON 형식 문자열 간의 변환을 처리하는 라이브러리이다. 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를 이용한 제외 처리암호와 같이 민감한 데이터의 경우 응답 결과에 포함시키지 않아야 한다. 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 형식으로 전송된 데이터를 올바르게 처리하려면 요청 컨텐츠 타입이 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이 발생하면 @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을 적용할 수 있다. |
Chapter 10: 스프링 MVC 프레임워크 동작 방식1.스프링 MVC 핵심 구성 요소와 각 요소 간의 관계
동작 과정
1.1 컨트롤러와 핸들러왜 컨트롤러를 찾아주는 객체는 'ControllerMapping'이 아니고 'HandlerMapping' 일까?
-> 컨트롤러라는 특정용어 쓰는 대신 스프링MVC가 지원하는 다양한 요청처리객체를 표현함
|
Chapter 8. DB 연동1. JDBC 프로그래밍의 단점을 보완하는 스프링자바의 DB 연동
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.gradledependencies {
.
.
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을 구한다.
Tomcat JDBC의 주요 프로퍼티Tomcat JDBC 모듈의 org.apache.tomcat.jdbc.pool.DataSource 클래스는 커넥션 풀 기능을 제공하는 DataSource 구현 클래스이다. DataSource 클래스는 커넥션을 몇 개 만들지 지정할 수 있는 메서드를 제공한다.
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이 발생한다.
변경 쿼리 실행 - update() 메서드INSERT, UPDATE, DELETE 쿼리는 update() 메서드를 사용한다.
update() 메서드는 쿼리 실행 결과로 변경된 행의 개수를 리턴한다. PreparedStatementCreator를 이용한 쿼리 실행PreparedStatement의 set 메서드를 사용해 직접 인덱스 파라미터의 값을 설정할 때도 있다. 이 경우 PreparedStatementCreator를 인자로 받는 메서드를 이용해서 직접 PreparedStatement를 생성하고 설정해야 한다. PreparedStatementCreator 인터페이스.
.
public interface PreparedStatementCreator {
PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}
.
.
PreparedStatementCreator 인터페이스를 파라미터로 갖는 메서드
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과 프록시@transactional 애노테이션이 적용된 빈 객체를 찾아서 알맞은 프록시 객체를 생성한다. ChangePasswordService 클래스의 메서드에 @transactional 애노테이션이 적용되어 있다고 하자.
@transactional 적용 메서드의 롤백 처리롤백을 처리하는 주체 또한 프록시 객체이다. 실제로 @transactional을 처리하기 위한 프록시 객체는 원본 객체의 메서드를 실행하는 과정에서 RuntimeException이 발생하면 트랜잭션을 롤백한다. 프록시가 트랜잭션을 롤백하는 경우
만약 RuntimeException을 상속하지 않는 Exception이 발생하면 어떻게 될까? @transactional의 rollbackFor 속성@Transactional(rollbackFor = SQLException.class)
public void someMethod() {
...
}
@transactional의 noRollbackFor 속성지정한 익셉션이 발생해도 롤백시키지 않고 커밋할 익셉션 타입을 지정할 때 사용한다. @transactional의 주요 속성
1. value 속성@transactional 애노테이션의 value 속성값이 없으면 등록된 빈 중에서 타입이 PlatformTransactionManager인 빈을 사용한다. 2. Propagation 속성
3. Isolation 속성트랜잭션 격리 레벨은 동시에 DB에 접근할 때 그 접근을 어떻게 제어할지에 대한 설정을 다룬다.
@EnableTransactionManagement의 주요 속성
JdbcTemplate은 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 범위에서 쿼리를 실행한다.public class ChangePasswordService {
...
@Transactional
public void changePassword(String email, String oldPwd, String newPwd) {
...
}
}
public class MemberDao {
...
// @Transactional 없음
public void update(Member member) {
...
}
}
|
굳 |
스프링 프로그래밍 입문 5 남은 부분 정독
The text was updated successfully, but these errors were encountered: