오늘은 주말이라 알라딘 중고매장을 방문했다.
평소처럼 프로그래밍 코너를 기웃기웃거리던 중 장바구니에 담아 둔 책을 발견했다!
(Yes24 카트에 담아뒀는데 알라딘 중고매장에서 구입하는 아이러니)
실전 자바 소프트웨어 개발 - 예스24
실전 프로젝트로 배우는 최신 자바 개발 기법 레벨업 가이드 이제 막 경력을 쌓기 시작한 자바 개발자가 최신 소프트웨어 개발 방법까지 알기란 벅찬 일이다. 테스트 주도 개발 같은 객체지향
www.yes24.com
책이 참 괜찮다.
나 같은 주니어 개발자에게 추천하고 싶은 책이다.
1. Notification 패턴
책의 코드를 따라치면서 Notification 패턴이라는 걸 봤다.
마틴 파울러가 정립한 개념이라고 한다.
그렇게 유명한 패턴은 아닌 듯 하다.
구글링 해봐도 관련 포스트를 하나 밖에 발견하지 못했다.
"java notification" 으로 검색하면 대부분 안드로이드의 notification이나 observer 패턴만 소개하고 있는 글들이다.
일단 GoF의 디자인 패턴에는 들어가 있지 않으니...
아무튼 해당 패턴은 어떤 때 쓰냐면,
여러 에러를 한 번에 수집하고, 이를 한 곳에서 처리하고 싶을 때 사용한다.
클라이언트 코드에서 예외를 개별적으로 처리하는 것보다 더 유연한 방법이라고 한다.
또한, 이 방식은 예외를 던지는 대신 에러를 수집하는 방식이므로,
프로그램의 흐름을 중단시키지 않고도 여러 검증 실패 사항을 동시에 다룰 수 있다고도 한다.
2. 기존 코드
먼저, 어떤 상황에서 적용하면 좋을지 예제 코드를 살펴 보자
package com.griotold.banktransactionanalyzersimple.exception;
import lombok.AllArgsConstructor;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
/**
* 검증 로직을 수행하는 Validator 클래스
*/
@AllArgsConstructor
public class BankStatementValidator {
private String description;
private String date;
private String amount;
// 과도하게 세밀한 검증
public boolean validateOverlySpecific() throws DescriptionTooLongException,
InvalidDateFormat,
DateInTheFutureException,
InvalidAmountException {
if (this.description.length() > 100) {
throw new DescriptionTooLongException();
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new InvalidDateFormat();
}
if (parsedDate.isAfter(LocalDate.now())) throw new DateInTheFutureException();
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new InvalidAmountException();
}
return true;
}
// 과도하게 덤덤한 검증 - 모든 예외를 IllegalArgumentException(런타임 예외)
public boolean validateRoughly() {
if (this.description.length() > 100) {
throw new IllegalArgumentException("The description is too long");
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("date cannot be in the future");
}
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid format for amount", e);
}
return true;
}
}
책의 2장, 3장에서 다루고 있는 입출금 내역 분석기 프로그램에 내용 중 일부이다.
입출금 내역을 입력받을 때 검증하는 Validator(검증기) 클래스의 모습인데
두 가지 안티 패턴을 사용하고 있다.
2 - 1. 과도하게 세밀한 검증 - 안티 패턴
// 과도하게 세밀한 검증
public boolean validateOverlySpecific() throws DescriptionTooLongException,
InvalidDateFormat,
DateInTheFutureException,
InvalidAmountException {
if (this.description.length() > 100) {
throw new DescriptionTooLongException();
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new InvalidDateFormat();
}
if (parsedDate.isAfter(LocalDate.now())) throw new DateInTheFutureException();
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new InvalidAmountException();
}
return true;
}
입력에서 발생할 수 있는 모든 예외상황을 체크드 익셉션(exception 상속)으로 정의해서 처리를 해주고 있다.
이 방법을 사용하면 너무 많은 설정 작업이 필요하고,
여러 예외를 선언해야 하며,
개발자가 모든 예외를 처리해줘야 한다.
2 - 2. 과도하게 대충 검증 - 안티 패턴
// 과도하게 덤덤한 검증 - 모든 예외를 IllegalArgumentException(런타임 예외)
public boolean validateRoughly() {
if (this.description.length() > 100) {
throw new IllegalArgumentException("The description is too long");
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("date cannot be in the future");
}
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid format for amount", e);
}
return true;
}
과도하게 세밀한 검증과는 반대로,
모든 예외 상황을 런타임 예외(IllegalArgumentException) 으로 퉁쳐버리는 형태
딱 봐도 이렇게 코드를 짜면 안 될 것 같은 느낌이 든다.
차라리 과도하게 세밀한 게 나아 보인다. (물론 런타임예외로 세밀하게 말이다.)
3. Notification 패턴 적용
앞서 본 두 가지 안티 패턴을 해결하는 Notification 패턴을 알아보자.
3 - 1. Notification 도메인 클래스
package com.griotold.banktransactionanalyzersimple.exception;
import java.util.ArrayList;
import java.util.List;
/**
* 오류를 수집하는 도메인 클래스
*/
public class Notification {
private final List<String> errors = new ArrayList<>();
public void addError(final String message) {
errors.add(message);
}
public boolean hasErrors() {
return !errors.isEmpty();
}
public String errorMessage() {
return errors.toString();
}
public List<String> getErrors() {
return this.errors;
}
}
도메인 클래스인 Notification으로 오류를 수집한다.
3 - 2. Validator 에 적용
// 노티피케이션 패턴 적용
public Notification validate() {
final Notification notification = new Notification();
if (this.description.length() > 100) {
notification.addError("The description is too long");
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
if (parsedDate.isAfter(LocalDate.now())) {
notification.addError("date cannot be in the future");
}
} catch (DateTimeParseException e) {
notification.addError("Invalid format for date");
}
final double amount;
try {
amount = Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
notification.addError("Invalid format for amount");
}
return notification;
}
Notification 객체를 만들어서 예외 상황마다 예외 메세지를 저장한 후 리턴해준다.
앞서 본 두 가지 안티 패턴은 여러 오류를 수집할 수 없었다.
하지만, Notification 패턴을 적용했더니 여러 오류를 사용자에게 전달할 수 있게 되었다.
근데 이런 패턴 어디서 많이 본 것 같은 느낌이다.
4. Spring Framework 의 BindingResult 와 Notification 사이의 유사점
바로 인프런 김영한님 강의에서
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 - 인프런
웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있
www.inflearn.com
@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() >
1000000) {
bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9,999 까지 허용합니다."));
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
Notification 패턴은 스프링 프레임워크의 BindingResult와 유사하다.
BindingResult는 스프링의 웹 어플리케이션에서 입력 데이터에 대한 검증 결과를 담는 객체로,
Controller 에서 입력값이 바인딩될 때 발생하는 오류들을 담는다.
여러 검증 오류를 하나의 객체에 모아두고,
그 결과를 뷰에 전달해서 사용자가 한 번에 모든 오류 메시지를 볼 수 있도록 해준다.
앞서 소개한 Notification 패턴도 BindingResult와 유사하게 동작한다.
둘 다 입력 데이터의 유효성 검증과 관련된 오류들을 효율적으로 관리하고,
사용자에게 상세한 피드백을 제공하기 위한 방법이라는 공통점이 있다.
따라서, BindingResult는 Notification 패턴을 적용한 사례라고 볼 수 있다.
5. 정리
이번 포스팅에서는 Notification 패턴에 대해서 알아보았다.
Spring Framework에서 자주 사용하던 BindingResult가 Notification 패턴을 적용한 것이라는 점은 처음 알게된 사실이다.
편리하게 사용하던 Framework에 이런 디자인 패턴이 숨어 있었다니...
흥미롭다.
오늘은 주말이라 알라딘 중고매장을 방문했다.
평소처럼 프로그래밍 코너를 기웃기웃거리던 중 장바구니에 담아 둔 책을 발견했다!
(Yes24 카트에 담아뒀는데 알라딘 중고매장에서 구입하는 아이러니)
실전 자바 소프트웨어 개발 - 예스24
실전 프로젝트로 배우는 최신 자바 개발 기법 레벨업 가이드 이제 막 경력을 쌓기 시작한 자바 개발자가 최신 소프트웨어 개발 방법까지 알기란 벅찬 일이다. 테스트 주도 개발 같은 객체지향
www.yes24.com
책이 참 괜찮다.
나 같은 주니어 개발자에게 추천하고 싶은 책이다.
1. Notification 패턴
책의 코드를 따라치면서 Notification 패턴이라는 걸 봤다.
마틴 파울러가 정립한 개념이라고 한다.
그렇게 유명한 패턴은 아닌 듯 하다.
구글링 해봐도 관련 포스트를 하나 밖에 발견하지 못했다.
"java notification" 으로 검색하면 대부분 안드로이드의 notification이나 observer 패턴만 소개하고 있는 글들이다.
일단 GoF의 디자인 패턴에는 들어가 있지 않으니...
아무튼 해당 패턴은 어떤 때 쓰냐면,
여러 에러를 한 번에 수집하고, 이를 한 곳에서 처리하고 싶을 때 사용한다.
클라이언트 코드에서 예외를 개별적으로 처리하는 것보다 더 유연한 방법이라고 한다.
또한, 이 방식은 예외를 던지는 대신 에러를 수집하는 방식이므로,
프로그램의 흐름을 중단시키지 않고도 여러 검증 실패 사항을 동시에 다룰 수 있다고도 한다.
2. 기존 코드
먼저, 어떤 상황에서 적용하면 좋을지 예제 코드를 살펴 보자
package com.griotold.banktransactionanalyzersimple.exception;
import lombok.AllArgsConstructor;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
/**
* 검증 로직을 수행하는 Validator 클래스
*/
@AllArgsConstructor
public class BankStatementValidator {
private String description;
private String date;
private String amount;
// 과도하게 세밀한 검증
public boolean validateOverlySpecific() throws DescriptionTooLongException,
InvalidDateFormat,
DateInTheFutureException,
InvalidAmountException {
if (this.description.length() > 100) {
throw new DescriptionTooLongException();
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new InvalidDateFormat();
}
if (parsedDate.isAfter(LocalDate.now())) throw new DateInTheFutureException();
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new InvalidAmountException();
}
return true;
}
// 과도하게 덤덤한 검증 - 모든 예외를 IllegalArgumentException(런타임 예외)
public boolean validateRoughly() {
if (this.description.length() > 100) {
throw new IllegalArgumentException("The description is too long");
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("date cannot be in the future");
}
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid format for amount", e);
}
return true;
}
}
책의 2장, 3장에서 다루고 있는 입출금 내역 분석기 프로그램에 내용 중 일부이다.
입출금 내역을 입력받을 때 검증하는 Validator(검증기) 클래스의 모습인데
두 가지 안티 패턴을 사용하고 있다.
2 - 1. 과도하게 세밀한 검증 - 안티 패턴
// 과도하게 세밀한 검증
public boolean validateOverlySpecific() throws DescriptionTooLongException,
InvalidDateFormat,
DateInTheFutureException,
InvalidAmountException {
if (this.description.length() > 100) {
throw new DescriptionTooLongException();
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new InvalidDateFormat();
}
if (parsedDate.isAfter(LocalDate.now())) throw new DateInTheFutureException();
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new InvalidAmountException();
}
return true;
}
입력에서 발생할 수 있는 모든 예외상황을 체크드 익셉션(exception 상속)으로 정의해서 처리를 해주고 있다.
이 방법을 사용하면 너무 많은 설정 작업이 필요하고,
여러 예외를 선언해야 하며,
개발자가 모든 예외를 처리해줘야 한다.
2 - 2. 과도하게 대충 검증 - 안티 패턴
// 과도하게 덤덤한 검증 - 모든 예외를 IllegalArgumentException(런타임 예외)
public boolean validateRoughly() {
if (this.description.length() > 100) {
throw new IllegalArgumentException("The description is too long");
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(this.date);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("date cannot be in the future");
}
try {
Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid format for amount", e);
}
return true;
}
과도하게 세밀한 검증과는 반대로,
모든 예외 상황을 런타임 예외(IllegalArgumentException) 으로 퉁쳐버리는 형태
딱 봐도 이렇게 코드를 짜면 안 될 것 같은 느낌이 든다.
차라리 과도하게 세밀한 게 나아 보인다. (물론 런타임예외로 세밀하게 말이다.)
3. Notification 패턴 적용
앞서 본 두 가지 안티 패턴을 해결하는 Notification 패턴을 알아보자.
3 - 1. Notification 도메인 클래스
package com.griotold.banktransactionanalyzersimple.exception;
import java.util.ArrayList;
import java.util.List;
/**
* 오류를 수집하는 도메인 클래스
*/
public class Notification {
private final List<String> errors = new ArrayList<>();
public void addError(final String message) {
errors.add(message);
}
public boolean hasErrors() {
return !errors.isEmpty();
}
public String errorMessage() {
return errors.toString();
}
public List<String> getErrors() {
return this.errors;
}
}
도메인 클래스인 Notification으로 오류를 수집한다.
3 - 2. Validator 에 적용
// 노티피케이션 패턴 적용
public Notification validate() {
final Notification notification = new Notification();
if (this.description.length() > 100) {
notification.addError("The description is too long");
}
final LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
if (parsedDate.isAfter(LocalDate.now())) {
notification.addError("date cannot be in the future");
}
} catch (DateTimeParseException e) {
notification.addError("Invalid format for date");
}
final double amount;
try {
amount = Double.parseDouble(this.amount);
} catch (NumberFormatException e) {
notification.addError("Invalid format for amount");
}
return notification;
}
Notification 객체를 만들어서 예외 상황마다 예외 메세지를 저장한 후 리턴해준다.
앞서 본 두 가지 안티 패턴은 여러 오류를 수집할 수 없었다.
하지만, Notification 패턴을 적용했더니 여러 오류를 사용자에게 전달할 수 있게 되었다.
근데 이런 패턴 어디서 많이 본 것 같은 느낌이다.
4. Spring Framework 의 BindingResult 와 Notification 사이의 유사점
바로 인프런 김영한님 강의에서
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 - 인프런
웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있
www.inflearn.com
@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() >
1000000) {
bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9,999 까지 허용합니다."));
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
Notification 패턴은 스프링 프레임워크의 BindingResult와 유사하다.
BindingResult는 스프링의 웹 어플리케이션에서 입력 데이터에 대한 검증 결과를 담는 객체로,
Controller 에서 입력값이 바인딩될 때 발생하는 오류들을 담는다.
여러 검증 오류를 하나의 객체에 모아두고,
그 결과를 뷰에 전달해서 사용자가 한 번에 모든 오류 메시지를 볼 수 있도록 해준다.
앞서 소개한 Notification 패턴도 BindingResult와 유사하게 동작한다.
둘 다 입력 데이터의 유효성 검증과 관련된 오류들을 효율적으로 관리하고,
사용자에게 상세한 피드백을 제공하기 위한 방법이라는 공통점이 있다.
따라서, BindingResult는 Notification 패턴을 적용한 사례라고 볼 수 있다.
5. 정리
이번 포스팅에서는 Notification 패턴에 대해서 알아보았다.
Spring Framework에서 자주 사용하던 BindingResult가 Notification 패턴을 적용한 것이라는 점은 처음 알게된 사실이다.
편리하게 사용하던 Framework에 이런 디자인 패턴이 숨어 있었다니...
흥미롭다.