네로개발일기

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

'전체 글'에 해당되는 글 194건


반응형

비동기, Promise, async, await 

비동기 - 동시에 여러 작업을 해야할 때

자바스크립트에서는 아무 일도 안하고 단순히 기다리기만 하는 함수가 있다.

바로 setTimeout 함수이다. 첫번째 인자는 기다린 후에 실행시킬 함수, 그 다음은 기다릴 밀리초이다.

console.log("모두에게 일을 시켜보자!");

setTimeout(() => {
  console.log("A: 일을 마쳤습니다!");
}, 1000);

setTimeout(() => {
  console.log("B: 일을 마쳤습니다!");
}, 1000);

setTimeout(() => {
  console.log("C: 일을 마쳤습니다!");
}, 1000);

console.log("일은 전부 시켜놓았다!");
모두에게 일을 시켜보자!
일은 전부 시켜놓았다!
A: 일을 마쳤습니다!
B: 일을 마쳤습니다!
C: 일을 마쳤습니다!

1초만의 시간만 걸린다. 일을 시켜놓기만 하면 알아서 진행한다.

 

우선, setTimeout 함수는 자체의 실행은 즉시 실행되고 리턴된다. setTimeout 함수가 즉시 종료되는 이유는 작업을 예약하는 일이 전부이기 때문이다. 이것에 관한 설명은 이벤트 루프를 참고하기 바란다.

 

setTimeout 함수를 통해 시간이 조금 걸리지만 기다리기만 하면 되는 작업을 흉내내 보았다.

1. setTimeout는 인자로 들어온 콜백 함수를 예약하기만 하고 바로 끝난다.

2. setTimeout에 의해 기다리는 동작은 본래의 코드 흐름과는 상관없이 따로 따로 독립적으로 돌아간다.

3. 이렇게 따로따로 독립적으로 돌아가는 작업을 비동기 작업이라고 한다.

 

어떤 작업들이 비동기로 진행될까?

브라우저에서는 ajax라 불리는 XMLHttpRequest 객체를 활용하여 비동기적으로 요청을 보내고 받을 수 있었고,

최근에는 Fetch API를 사용하는 방법이 늘고있다.

 

비동기 작업의 문제점 (1) - 흐름을 예측하기 어렵다.

동기적으로 동작한다는 건, 시간이 오래 걸리긴 하지만 무엇이 어떻게 진행될 지는 명확하다.

반면, 비동기 코드에서는 비교적 효율적이긴 하지만, 무엇이 어떤 순서로 진행될지 예측이 어렵다.

 

비동기 작업의 문제점 (2) - 콜백 지옥

비동기 작업 특성상, 각각의 비동기 작업이 끝났을 때 이어질 작업을 미리 부여하는 식으로 흐름을 제어한다.

비동기 작업이 차례대로 주루룩 이어져 있다면, 이는 콜백 지옥에 해당한다.

종종 콜백 지옥에 의해 Promise가 등장했다는 설명도 있다. (반은 맞고 반은 틀린 설명이다.) 콜백을 통해 다음 할 일을 정하는 개념은 Promise에서도 동일하다. 콜백 지옥이라는 상황이 벌어지는 이유는 비동기 작업을 관리한다는 개념 자체가 없어서 코드가 지저분해질 수 밖에 없기 때문이다.

 

Promise

Promise는 비동기 작업의 단위이다. Promise를 통해 어떻게 비동기 작업들을 쉽게 관리할 수 있는지 알아보자.

기본 사용법

우선 Promise로 관리할 비동기 작업을 만들 때에는 Promise에서 요구하는 방법대로 만들어야 한다. new Promise(...)

const promise1 = new Promise((resolve, reject) => {
  // 비동기 작업
});

1. 변수의 이름은 promise1이며, const로 선언했기 때문에 재할당이 되지 않는다. 하나의 변수로 끝까지 해당 Promise를 관리하는 것이 가독성도 좋고 유지보수 하기도 좋다.

2. new Promise(...)로 Promise 객체를 새로 만들었다. 생성자는 함수와 동일하게 동작하므로 괄호를 사용하여 함수를 호출한다.

3. 생성자는 특별한 함수를 인자로 받는다. 

이 특별한 함수는 공식문서에서 executor라는 이름으로 부른다. 이 함수에 대해 자세히 설명하면 다음과 같다.

1. executor는 첫번째 인수로 resolve, 두번째 인수로 reject를 받는다.

2. resolve는 executor내에서 호출할 수 있는 또 다른 함수이다. resolve를 호출하게 된다면 비동기 작업이 성공했다는 뜻입니다.

3. reject 또한 executor 내에서 호출할 수 있는 또 다른 함수이다. reject를 호출하게 된다면 비동기 작업이 실패했다는 뜻입니다.

 

Promise의 특징으로 new Promise(...)하는 순간 여기에 할당된 비동기 작업은 바로 시작된다.

함수는 으레 정의하는 시점과 호출하는 시점이 다르다고 했지만 new Promise(...)는 바로 호출한다.

비동기 작업의 특징은 작업이 언제 끝날지 모른다. 이 작업이 성공하거나 실패하는 순간에 뒷처리를 해줘야 한다.

Promise 가 끝나고 다음의 동작을 설정해줄 수 있는데 바로 then 메서드와 catch 메서드다.

- then 메서드는 해당 Promise가 성공했을 때의 동작을 지정한다. 인자로 함수를 받는다.

- catch 메서드는 해당 Promise가 실패했을 때의 동작을 지정한다. 인자로 함수를 받는다.

- 위 함수는 체인 형태로 활용할 수 있다. 

 

const promise1 = new Promise((resolve, reject) => {
  resolve();
});

promise1
  .then(() => {
    console.log("then!");
  }).catch(() => {
    console.log("catch!");
  });

executor로 새로운 Promise를 만든 다음 then과 catch를 이용하여 후속 동작까지 지정해줘야 한다.

위 코드의 실행 결과는 다음과 같다.

then!

resolve 부분을 reject로 수정하였다. 

const promise1 = new Promise((resolve, reject) => {
  reject();
});

promise1
  .then(() => {
    console.log("then!");
  })
  .catch(() => {
    console.log("catch!");
  });

이것의 실행결과는 다음과 같다.

catch!

재사용하기

new Promise(...)를 하는 순간 비동기 작업이 시작되는데, 비슷한 비동기 작업을 수행할 때마다 매번 new Promise(...)를 해주어야 할까? new Promise(...) 한 것을 그대로 리턴하는 함수를 만들어 사용하면 된다.

function startAsync(age) {
  return new Promise((resolve, reject) => {
    if (age > 20) resolve();
    else reject();
  });
}

const promise1 = startAsync(25);
promise1
  .then(() => {
    console.log("1 then!");
  })
  .catch(() => {
    console.log("1 catch!");
  });

const promise2 = startAsync(15);
promise2
  .then(() => {
    console.log("2 then!");
  })
  .catch(() => {
    console.log("2 catch!");
  });
1 then!
2 catch!

작업 결과를 전달하기

resolve, reject 함수에 인자를 전달함으로써 then 및 catch 함수에서 비동기 작업으로부터 정보를 얻을 수 있다.

function startAsync(age) {
  return new Promise((resolve, reject) => {
    if (age > 20) resolve(`${age} success`);
    else reject(new Error(`${age} is not over 20`));
  });
}

const promise1 = startAsync(25);
promise1
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.error(error);
  });

const promise2 = startAsync(15);
promise2
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.error(error);
  });
25 success
Error: 15 is not over 20
  at file
  at new Promise(<anonymous>)
  ...

기타 고려사항

- executor 내부에서 에러가 throw 된다면 해당 에러로 reject 가 수행된다.

- executor의 리턴값은 무시된다.

- 첫번째 resolve, reject만 유효하다.

const throwError = new Promise((resolve, reject) => {
  throw Error("error");
});
throwError
  .then(() => console.log("throwError succeess"))
  .catch(() => console.log("throwError catched")); //
  
const ret = new Promise((resolve, reject) => {
  return "returned");
});
ret
  .then(() => console.log("ret success")) //
  .catch(() => console.log("ret catched"))

// resolve 만 됩니다.
const several1 = new Promise((resolve, reject) => {
  resolve();
  reject();
});
several1
  .then(() => console.log("several1 success"))
  .catch(() => console.log("several1 catched"));

// reject 만 됩니다.
const several2 = new Promise((resolve, reject) => {
  reject();
  resolve();
});
several2
  .then(() => console.log("several2 success"))
  .catch(() => console.log("several2 catched"));

// resolve 만 됩니다.
const several3 = new Promise((resolve, reject) => {
  resolve();
  throw new Error("error");
});
several3
  .then(() => console.log("several3 success"))
  .catch(() => console.log("several3 catched"));

Promise의 의의

Promise는 세 가지 상태를 가진다.

- 대기 (pending)

- 이행 (fulfilled)

- 거부 (rejected)

이행 상태 일때 then, 거부 상태일 때 catch로 등록한 동작들이 실행됩니다. 

Promise를 생성한 이후에 상태가 실제로 어떤지 확인은 할 수 업다. 자바스크립트를 실행하는 브라우저 혹은 node.js에서 알아서 관리하기 때문이다.

Promise는 비동기 작업을 생성/시작하는 부분 (new Promise(...))과 작업 이후의 동작 지정부분 (then, catch)을 분리함으로써 유연한 설계를 가능토록 한다.

 

async - 비동기 작업을 만드는 손쉬운 방법

async 키워드는 함수를 선언할 때 붙여줄 수 있다. async 키워드가 붙은 함수를 async 함수, 없는 함수를 일반 함수라고 부르도록 하자. 

async 함수는 Promise와 밀접한 연관을 가지고 있는데, 기존에 작성한 executor로부터 몇가지 규칙만 적용한다면 new Promise(...)를 리턴하는 함수를 async 함수로 손쉽게 변환할 수 있다.

- 함수에 async 키워드를 붙인다.

- new Promise (...) 부분을 없애고 executor 본문 내용만 남긴다.

- resolve(value); 부분을 return value; 로 변경한다.

- reject(new Error(...)); 부분을 throw new Error(...); 로 수정한다.

 

// 기존
// function startAsync(age) {
//   retrurn new Promise((resolve, reject) => {
//     if (age > 20) resolve(`${age} success`);
//     else reject(new Error(`${age} is not over 20`));//   
//   });
// }

async function startAsync(age) {
  if (age > 20) return `${age} success`;
  else throw new Error(`${age} is not over 20`);
}

const promise1 = startAsync(25);
promise1
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.error(error);
  });

const promise2 = startAsync(15);
promise2
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.error(error);
  });

실행하면 다음과 같다.

25 success
Error: 15 is not over 20
    at startAsync
    ...

놀랍게도 똑같이 동작한다.

이런 결론을 얻을 수 있다.

async 함수의 리턴값은 무조건 Promise이다.

우리는 async 함수를 일반 함수처럼 사용할 수 없다는 걸 절실히 깨닫게 되었다. 우리는 무조건 async 함수를 실행시킨 뒤 then과 catch 를 활용하여 흐름을 제어해야 한다.

한가지 사소하게 다른 점은, 에러메시지가 두 줄 줄어들었다. new Promise(<anonymous>) 부분이 사라졌다.

 

모든 비동기 동작을 async 함수로 만들 수 있는 건 아니다.

 

await - Promise 가 끝날 때까지 기다리라.

awaited는 Promise 가 이행되든지 거절되든지 끝날 때까지 기다리는 함수이다. await는 async 함수 내부에서만 사용할 수 있다.

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

async function startAsync(age) {
  if (age > 20) return `${age} success`;
  else throw new Error(`${age} is not over 20`);
}

async function startAsyncJobs() {
  await setTimeoutPromise(1000);
  const promise1 = startAsync(25);
  
  try {
    const value = await promise1;
    console.log(value);
  } catch (e) {
    console.error(e);
  }
  
  const promise2 = startAsync(15);
  
  try {
    const value = await promise2;
    console.log(value);
  } catch (e) {
    console.error(e);
  }
}

startAsyncJobs();
// 1초후
25 success
Error: 15 is not over 20
    at startAsync
    at startAsyncJobs

1. 문법적으로 await [[Promise 객체]] 사용한다.

2. await은 Promise가 완료될 때까지 기다린다. 그러므로 setTimeoutPromise의 executor에서 resolve 함수가 호출될 때까지 기다립니다. 그 동안 startAsyncJobs의 진행은 멈춰있다.

3. await은 Promise 가 resolve 한 값을 내놓는다. async 함수 내부에서는 리턴하는 값을 resolve한 값으로 간주하므로 value엔 ${age| success가 들어간다.

4. 해당 Promise에서 reject가 발생한다면 예외가 발생한다. 이 예외를 처리하기 위해선 try-catch 구문을 사용한다. reject로 넘긴 에러 (async 함수 내에서는 throw한 에러)는 catch 절로 넘어간다. 

 

await는 왜 async 함수에서만 사용할 수 있을까?

비동기 작업으로부터 파생된 모든 작업은 비동기 작업으로 간주할 수 있다. 동기적으로 실행되는 프로그램에서 비동기 작업이 시작되었을 때, 그것을 기다리는 행위는 무의미하다.

반면, 비동기 환경에서 비동기 작업의 결과를 기다리는 것은 의미가 있다. 비동기는 동작 특성상 실제 작업과 그 작업의 후속 조치를 분리할 수밖에 없는데, async와 await를 쓰면 하나의 흐름 속에서 코딩할 수 있다.

 

.then 혹은 await가 없다면 어떻게 될까?

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

async function startAsync() {
  setTimeoutPromise(1000);
  setTimeoutPromise(1500);
  setTimeoutPromise(2000);
}

console.log("시작");

const promise = startAsync();
promise
  .then(() => {
    console.log("끝");
    process.exit(0);
  });

위 코드는 실행되자마자 아래 출력을 남기고 즉시 종료된다.

시작
끝

startAsync 함수의 문제점은 명확하다. setTimeoutPromise에 await를 걸지 않았다는 것이다. 

await 을 적절히 걸어야 한다는 감각은 처음 비동기를 익힐 때 쉽사리 적응되지 않는 감각이므로, 자주 쓰면서 습관을 만들어낼 수 밖에 없다.

 

 참고하면 좋은 글 

https://frogand.tistory.com/105

 

[ES6] 프로미스 (Promise)

1. 프로미스란? 자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리

frogand.tistory.com

- 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 네스팅(nesting, 중첩)되어 복잡도가 높아지는 콜백 헬(Callback Hell)이 발생하는 단점이 있다. 

- 이때 비동기 처리가 성공하면 콜백 함수의 인자로 전달받은 resolve 함수를 호출한다. 이때 프로미스는 'fulfilled' 상태가 된다. 비동기 처리가 실패하면 reject 함수를 호출한다. 이때 프로미스는 'rejected' 상태가 된다. 

 

 출처 

https://springfall.cc/post/7

 

[Javascript] 비동기, Promise, async, await 확실하게 이해하기

봄가을 개발 블로그

springfall.cc

 

728x90
반응형
blog image

Written by ner.o

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

반응형

스프링은 애플리케이션 전 계층에서 도메인 객체를 검증할 수 있는 인터페이스를 제공한다. 스프링의 bean validation을 통해 controller의 파라미터를 비즈니스 로직을 추가하지 않고 검증할 수 있는지 알아보자.

 

interface Validator

Spring은 도메인 객체를 검증할 수 있도록 Validator 인터페이스를 도입했다. Validator 인터페이스는 객체를 검증하는데 실패하면 Errors 객체에 에러를 등록한다.

Validator 인터페이스는 아래의 두가지 메서드를 가지고 있다.

- supports(Class): 매개변수로 전달된 클래스를 검증할 수 있는지 여부를 반환

- validate(Object, org.springframework.validation.Errors): 매개변수로 전달된 객체를 검증하고 실패하면 Errors 객체에 에러를 등록한다.

 

아래 코드는 Person 객체가 어떤 식으로 Validator 인터페이스를 구현하는지 보여준다.

@Getter @Setter
public class Person {
    private String name;
    private int age;
}
public class PersonValidator implements Validator {

    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }
    
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negative.value");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.old");
        }
    }
}

validate 함수를 보면, 검증에 실패한 경우 Errors 객체의 rejectValue 함수를 호출하는 것을 볼 수 있다.

rejectValue의 파라미터는 필드이름, 에러코드로 구성된다.

ValidationUtils 클래스를 이용하여 필드 검증을 하는데, 값이 비어있거나 공백문자가 있는 경우를 쉽게 확인할 수 있다.

 

위 코드를 테스트 해보자.

@Autowired
private PersonValidator personValidator;

@GetMapping("/person/validate")
public boolean directlyValidatePerson(@ModelAttribute Person person, BindingResult result) {
    logger.debug("validate directly. {}", person);
    
    personValidator.validate(person, result);
    
    return !result.hasErrors();
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = { WebAppConfig.class, ValidatorConfig.class })
public class PersonValidateControllerTest {
    
    @Autowired 
    private PersonValidator personValidator;
    
    private MockMvc mockMvc;
    
    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new PersonValidateController(personValidator)).build();
    }
    
    @Test
    public void directlyValidateEmptyNameTest() throws Exception {
        this.mockMvc.perform(get("/person/validate")
                             .param("name", "")
                             .param("age", "25")
                     .andDo(print())
                     .andExpect(status().isOk())
                     .andExpect(content().string("false"));
    }
    
    @Test
	public void directlyValidateWrongAgeTest() throws Exception {
		// 음수 나이
		this.mockMvc.perform(get("/person/validate")
				.param("name", "test")
				.param("age", "-1"))
			.andDo(print())
			.andExpect(status().isOk())
			.andExpect(content().string("false"));

		// 100살을 초과하는 나이
		this.mockMvc.perform(get("/person/validate")
				.param("name", "test")
				.param("age", "101"))
			.andDo(print())
			.andExpect(status().isOk())
			.andExpect(content().string("false"));
	}
}

 

directlyValidatePerson 메서드를 보면 파라미터에 Person 객체 외에 BindingResult 객체가 있다.

BindingResult 객체는 모델의 검증 작업에서 나타난 에러를 저장하는 역할을 하며, 이를 이용해 메서드 실행을 분기할 수 있다.

 

MessageSource

위 Validator의 validate 함수 내에서 검증 실패시 reject 함수를 호출하는 것을 살펴봤다. 이 때 reject 함수 파라미터 중 에러코드를 지정했는데, 이는 에러메시지와 관련이 있다.

에러 메시지는 보통 messages.properties와 같은 properties 파일에서 읽어오도록 구현한다. Spring에서는 MessageSource를 이용해 properties 파일로부터 에러 메시지를 가져오도록 할 수 있다. 

 

ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");

위 코드에서 name.empty는 에러코드로 messages.properties 파일에 존재하는 키 값이다.

messages.properties에는 다음과 같이 선언이 되어있다.

name.empty=The name is empty

이 값을 가져오기 위해 MessageSource를 사용하는데, MessageSource의 구현에는 두가지 방법이 있다.

- StaticMessageSource: 코드로 메시지를 등록한다.

- ResourceBundleMessageSource: 리소스 파일로부터 읽어와 등록한다.

 

properties 파일을 읽어와 에러 메시지를 읽어올 것이므로 ResourceBundleMessageSource 클래스를 이용하자.

@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
    resourceBundleMessageSource.setBasename("messages");
    resourceBundleMessageSource.setDefaultEncoding("UTF-8");
    
    return resourceBundleMessageSource;
}
@Autowired
private MessageSource messageSource;

@GetMapping("/person/validate")
public List<String> validateAndGetErrorMessages(@ModelAttribute Person person, BindingResult result) {
    logger.debug("validated. {}", person);
    
    personValidator.validate(person, result);
    
    return result.getFieldErrors().stream()
                            .map(e -> messageSource.getMessage(e, Locale.getDefault()))
                            .collect(Collectors.toList());
}
@Test
public void validateAndGetBindingResultTest() throws Exception {
    mockMvc.perform(get("/person/validate")
                    .param("name", "")
                    .param("age", "99")
           .andDo(print())
           .andExpect(status().isOk());
}

이 테스트에 대한 결과는 아래와 같다.

MockHttpServletResponse:
               Status = 200
        Error message = null
              Headers = [Content-type: "application/json;charset=UTF-8"]
         Content Type = application/json;charset=UTF-8
                 Body = ["The name is empty"]
        Forwarded URL = null
       Redirected URL = null
              Cookies = []

 

Spring Validation

Spring 3 부터 Bean Validation API를 제공한다. Spring Bean Validation은 Validator 인터페이스를 이용하여 직접 도메인 객체를 검증하는 방법을 표준화하고 어노테이션을 이용해 표현할 수 있도록 도와준다.

Bean Validation 명세는 구현체로 Hibernate Validator를 지원한다.

Hibernate Validator는 자주 쓰이는 몇가지 검증 어노테이션을 built-in으로 제공한다.

@Getter @Setter
public class Car {

    @NotBlank(message = "The manufacturer must not be empty.")
    private String manufacturer;
    
    @Range(min = 0, max = 10, message = "The seat count must be between 0 ~ 10")
    private int seatCount;
    
    @Range(min = 0, max = 300, message = "The speed must be between 0 ~ 300")
    private int topSpeed;
}

세가지 필드를 가지고 있는 간단한 도메인 클래스이다. 검증 조건을 어노테이션으로 선언하여 표현하였다. 이를 검증하는 Validator를 가지고 와야하는데 Bean Validation API에서 LocalValidatorFactoryBean 객체를 기본으로 제공해줘 이를 이용해 Validator 인터페이스의 bean을 생성할 수 있다. 이 객체는 org.springframework.validation.Validator 인터페이스 뿐만 아니라, javax.validation.ValidatorFactory, javax.validation.Validator 인터페이스를 모두 구현하고 있다. 이 bean을 이용해 애플리케이션 전 계층에서 객체를 검증할 수 있다. 아래는 Validator를 등록하는 과정이다.

@Bean
public Validator jsrValidator() {
    return new LocalValidatorFactoryBean();
}

이 Validator를 통해 Car 객체를 검증하는 컨트롤러 메서드를 작성하자.

@Autowired
@Qualifier("jsrValidator")
private Validator validator;

@GetMapping("/car/validate")
public boolean directlyValidateCar(@ModelAttribute Car car, BindingResult result) {
    logger.debug("validate directly. {}", car);
    
    validator.validate(car, result);
    logger.debug("errors: {}", result.getFiledErrors());
    
    return !result.hasErrors();
}
@Test
public void directlyValidateTest() throws Exception {
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "kook")
                        .param("seatCount", "4")
                        .param("topSpeed", "200"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("true"));
}

@Test
public void directlyValidateInvalidParamTest() throws Exception {
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "kook")
                        .param("seatCount", "4")
                        .param("topSpeed", "301"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("false"));
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "kook")
                        .param("seatCount", "-1")
                        .param("topSpeed", "301"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("false"));
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "")
                        .param("seatCount", "4")
                        .param("topSpeed", "301"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("false"));
}

위 코드에서 검증 후, 검증 에러의 로그를 찍도록 했는데 Car 클래스의 검증 어노테이션의 message 속성으로 정의한 에러메시지가 표시된다.

 

Spring MVC 3 Validation

Spring3부터 Spring MVC 컨트롤러 메서드의 파라미터를 자동으로 검증하는 어노테이션의 @Valid를 제공해준다. 지금까지는 직접 도메인 객체를 검증하는 방법이었지만, @Valid 어노테이션을 사용하면 검증하는 로직이 없어도 자동으로 검증해준다.

@Autowired
private MessageSource messageSource;

@GetMapping("/validate")
public boolean automaticallyValidateCar(@ModelAttribute @Valid Car car) {
    logger.debug("validate automatically. {}", car);
    
    return true;
}

@ExceptionHandler({BindException.class})
public ResponseEntity<String> paramValidateError(BindException ex) {
    logger.error(ex.getMessage());
    return ResponseEntity.badRequest()
                         .body(messageResource.getMessage(ex.getFielderror(), Locale.getDefault()));
}

위 코드에서 컨트롤러 메서드에서 BindingResult 객체를 받지 않는 것을 볼 수 있다. @Valid 애노테이션을 붙인 컨트롤러 메서드도 마찬가지로 검증 에러를 BindingResult 객체에 저장을 하는데, 만약 컨트롤러 메서드가 BindingResult 객체를 받지 않는다면 검증에러 발생시, BindException을 내보낸다.

이 예외는 HTTP 상태코드 400으로 처리되는데, 위 코드에서는 BindException을 400으로 처리는 그대로 하지만, body에 에러메시지를 담아서 내보내기 위해 ExceptionHandler 메서드를 정의하였다.

 

 

 

 

 

 

 출처 

https://lazymankook.tistory.com/86

 

Spring Validation

스프링은 애플리케이션 전 계층에서 도메인 객체를 검증할 수 있는 인터페이스를 제공한다. 이번 글에서는 spring의 bean validation을 통해 어떻게 controller의 파라미터를 controller에 비즈니스 로직을

lazymankook.tistory.com

 

728x90
반응형
blog image

Written by ner.o

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

반응형

Java에서 Enum을 활용적으로 !

 

Enum을 통해 얻는 기본적인 장점들은 아래와 같다.

1. 문자열과 비교해, IDE의 적극적인 지원을 받을 수 있다.

- 자동완성, 오타검증, 텍스트 리팩토링

2. 허용 가능한 값들을 제한할 수 있다.

3. 리팩토링 시, 변경 범위가 최소화된다.

- 내용의 추가가 필요하더라도, Enum 코드 외에 수정할 필요가 없다.

 

이 장점들은 모든 언어들의 Enum에서 얻을 수 있는 공통적인 장점이다.

 

하지만, Java의 Enum은 이보다 더 많은 장점을 가지고 있다.

Java의 Enum은 완전한 기능을 가진 클래스이기 때문이다. (C/C++의 경우 Enum이 결국 int 값이다.)

 

1. 데이터들간의 연관관계 표현

origin 테이블에 있는 내용을 2개의 테이블 (테이블명: table1, table2)에 등록하는 기능이 있다고 하자.

origin 테이블의 값은 "Y", "N"으로 저장되는데 table1, table2는 각각 "1", "0" / true, false 형태로 저장된다고 한다.

그럼 이를 분류하는 메서드를 다음과 같이 만들 것이다.

public class LegacyCase {
    
    public String toTable1Value(String originValue) {
        if ("Y".equals(originValue)) {
            return "1";
        } else {
            return "0";
        }
    }
    
    public boolean toTable2Value(String originValue) {
        if ("Y".equals(originValue)) {
            return true;
        } else {
            return false;
        }
    }
}

기능상의 문제는 없지만, 문제가 있다.

1. "Y", "1", true가 모두 같은 의미라는 것을 알 수 없다.

"Y"란 값이 "1"이 될 수도 있고 true가 될 수도 있다는 것을 확인하려면 항상 위에서 선언된 클래스와 메서드를 찾아야 한다.

2. 불필요한 코드량이 많다.

- "Y", "N"이외에 "R", "S" 등의 추가 값이 필요한 경우, if문을 포함하여 메서드 단위로 코드가 증가하게 된다.

- 동일한 타입의 값이 추가되는 것에 비해 너무 많은 반복성 코드가 발생하게 된다.

 

그래서 이 부분을 Enum으로 추출해보자.

public enum TableStatus {
    
    Y("1", true);
    N("0", false);
    
    private String table1Value;
    private boolean table2Value;
    
    TableStatus(String table1Value, boolean table2Value) {
        this.table1Value = table1Value;
        this.table2Value = table2Value;
    }
    
    public String getTable1Value() {
        return table1Value;
    }
    
    public boolean getTable2Value() {
        return table2Value;
    }
}

"Y", "1", true 가 한 묶음으로, "N", "0", false가 한 묶음이 된 것을 코드로 확인할 수 있다.

또한 추가 타입이 필요한 경우에도, Enum 상수와 get 메서만 추가하면 된다.

(lombok의 @Getter 를 사용하여 Enum의 get 메서드를 대신할 수 있다.)

 

@Test
public void test() throws Exception {
    
    // given
    TableStatus origin = selectFromOriginTable();
    
    String table1Value = origin.getTable1Value();
    String table2Value = origin.getTable2Value();
    
    assertThat(origin, is(TableStatus.Y));
    assertThat(table1Value, is("1"));
    assertThat(table2Value, is(true));
}

 

2. 상태와 행위를 한 곳에서 관리

서로 다른 계산식을 적용해야할 때가 있다.

DB에 저장된 code의 값이 "CALC_A"일 경우엔 값 그대로, "CALC_B"일 경우에 10을 곱해서, "CALC_C"인 경우에는 3을 곱해서 전달해야 한다.

가장 쉬운 해결방법은 아래와 같이 static 메서드를 작성해서 필요한 곳에 호출하는 방식이다.

public class LegacyCalculator {
    
    public static long calculate(String code, long originValue) {
        if ("CALC_A".equals(code)) {
            return originValue;
        } else if ("CALC_B".equals(code)) {
            return originValue * 10;
        } else if ("CALC_B".equals(code)) {
            return originValue * 3;
        } else {
            return 0;
        }
    }
}

이렇게 메서드를 분리하고 사용하다 보면, 코드는 코드대로 조회하고, 계산은 별도의 클래스와 메서드에서 진행하게 된다.

@Test
public void 코드에_따른_서로다른_계산_legacy() throws Exception {
    String code = selectCode();
    long originValue = 10000L;
    long result = LegacyCalculator.calculator(code, originValue);
    
    assertThat(result, is(10000L));
}

이 상황에 문제가 있다.

LegacyCalculator의 메서드와 code가 서로 관계가 있음을 표현할 수 없기 때문이다.

이런 경우,

1. 똑같은 기능을 하는 메서드를 중복 생성할 수 있다.

- 히스토리 관리가 되지 않은 상태에서 계산 메서드를 중복 생성할 수 있다.

- 관리 포인트가 증가할 확률이 매우 높다.

2. 계산 메서드를 누락할 수 있다.

- 결국 문자열과 메서드로 분리되어 있어, 이 계산 메서드를 써야함을 알 수 없어 새로운 기능 생성 시 계산 메서드 호출이 누락될 수 있다.

 

"DB에서 뽑은 특정 값은 지정된 메서드와 관계가 있다."

역할과 책임이라는 관점에서 봤을 때, 위 메시지는 Code에 책임이 있다.

public class CalculatorType {

    CALC_A(value -> value),
    CALC_B(value -> value * 10),
    CALC_C(value -> value * 3),
    CALC_ETC(value -> 0L);
    
    private Function<Long, Long> expression;
    
    CalculatorType(Function<Long, Long> expression) {
        this.expression = expression;
    }
    
    public long calculate(long value) {
        return expression.apply(value);
    }
}

Code가 본인 만의 계산식을 가지도록 지정하였다. (Java8이 업데이트되면서, 인자값으로 함수를 사용할 수 있게 되었다.)

 

Entity 클래스에서 선언할 경우에 String이 아니라 enum을 선언하면 된다.

@Column
@Enumerated(EnumType.STRING)
private CalculatorType calculatorType;

실제로 사용하는 곳에서도 직접 Code에게 계산을 요청하자.

@Test
public void 코드에_따라_서로다른_계산_enum() throws Exception {
    CalculatorType code = selectCode();
    long originValue = 10000L;
    long result = code.calculate(originValue);
    
    assertThat(result, is(10000L));
}

3. 데이터 그룹관리

결제라는 데이터는 '결제 종류'와 '결제 수단'이라는 2가지 형태로 표현된다.

예를 들어 신용카드 결제는 '신용카드'라는 결제 수단과 '카드'라는 결제 종류가 있다.

 

결제된 건이 어떤 결제 수단으로 진행되며, 결제 방식이 어떤 결제 종류에 속하는지 확인해야한다고 하자.

이를 해결하는 가장 쉬운 방법은 if문이다.

public class LegacyPayGroup {
    
    public static String getPayGroup(String payCode) {
        
        if ("ACCOUNT_TRANSFER".equals(payCode) || "PERMITTANCE".equals(payCode) || "ON_SITE_PAYMENT".equals(payCode) || "TOSS".equals(payCode)) {
            return "CASH";
        } else if ("PAYCO".equals(payCode) || "CARD".equals(payCode) || "KAKAO_PAY".equals(payCode) || "BAEMIN_PAY".equals(payCode)) {
            return "CARD";
        } else if ("POINT".equals(payCode) || "COUPON".equals(payCode)) {
            return "ETC";
        } else {
            return "EMPTY";
        }
    }
}

여기서도 여러 문제가 있다.

1. 둘의 관계를 파악하기 힘들다.

- 위 메서드는 포함관계를 나타내는 것일까. 아니면 단순한 대체값을 리턴한 것일까?

- 현재는 결제 종류가 결제 수단을 포함하고 있는 관계인데, 메서드만으로 표현이 불가능하다.

2. 입력값과 결과값이 예측 불가능하다.

- 결제 수단의 범위를 지정할 수 있어서 문자열이면 전부 파라미터로 전달될 수 있다.

- 마찬가지로 결과를 받는 쪽에서도 문자열을 받기 때문에 결제 종류로 지정된 값만 받을 수 있도록 검증코드가 필요하게 된다.

3. 그룹별 기능을 추가하기가 어렵다.

- 또 다시 결제 종류에 따른 if문으로 메서드를 만들어야 하나?

 

각각의 메서드는 원하는 때에 사용하기 위해 독립적으로 구성할 수 밖에 없는데

그럴 때마다 결제 종류를 분기하는 코드가 필수적으로 필요하다. 

 

각 타입은 본인이 수행해야할 기능과 책임과 가질 수 있게 하기 위해 enum 타입으로 변경하자.

@Getter
public enum PayGroup {

    CASH("현금", Arrays.asList("ACCOUNT_TRANSFER", "REMITTANCE", "ON_SITE_PAYMENT", "TOSS")),
    CARD("카드", Arrays.asList("PAYCO", "CARD", "KAKAO_PAY", "BAEMIN_PAY")),
    ETC("기타", Arrays.asList("POINT", "COUPON")),
    EMPTY("없음", Collections.EMPTY_LIST);
    
    private String title;
    private List<String> payList;
    
    PayGroup(String title, List<String> payList) {
        this.title = title;
        this.payList = payList;
    }
    
    public static PayGroup findByPayCode(String code) {
        return Arrays.stream(PayGroup.values())
                .filter(payGroup -> payGroup.hasPayCode(code))
                .findAny()
                .orElse(EMPTY);
    }
    
    public boolean hasPayCode(String code) {
        return payList.stream()
                .anyMatch(pay -> pay.equals(code));
    }
}

Java의 Enum은 결국 클래스이기 때문에 Enum 상수에 결제 종류 문자열 리스트를 가지도록 했다.

@Test
public void PayGroup에게_직접_결제종류_물어보기_문자열() throws Exception {
    String payCode = selectPayCode();
    PayGroup payGroup = PayGroup.findByPayCode(payCode);
    
    assertThat(payGroup.name(), is("BAEMIN_PAY"));
    assertThat(payGroup.getTitle(), is("배민페이"));
}

하지만, 해결되지 않은 것이 있다.

결제수단은 문자열로 되어있다.

DB 테이블의 결제수단 컬럼에 잘못된 값을 등록하거나, 파라미터로 전달된 값이 잘못되었을 경우가 있을 때 관리가 되지않을 위험이 있다.

public enum PayType {

    ACCOUNT_TRANSFER("계좌이체"),
    REMITTANCE("무통장입금"),
    ON_SITE_PAYMENT("현장결제"),
    TOSS("토스"),
    PAYCO("페이코"),
    CARD("신용카드"),
    KAKAO_PAY("카카오페이"),
    BAEMIN_PAY("배민페이"),
    POINT("포인트"),
    COUPON("쿠폰"),
    EMPTY("없음");
    
    private String title;
    
    PayType(String title) {
        this.title = title;
    }
    
    public String getTitle() {
        return title;
    }
}

이렇게 Enum으로 결제종류를 만들고 PayGroup에서 사용해보자.

@Getter
public enum PayGroupAdvanced {

    CASH("현금", Arrays.asList(PayType.ACCOUNT_TRANSFER, PayType.REMITTANCE, PayType.ON_SITE_PAYMENT, PayType.TOSS)),
    CARD("카드", Arrays.asList(PayType.PAYCO, PayType.CARD, PayType.KAKAO_PAY, PayType.BAEMIN_PAY)),
    ETC("기타", Arrays.asList(PayType.POINT, PayType.COUPON)),
    EMPTY("없음", Collections.EMPTY_LIST);
    
    private String title;
    private List<String> payList;
    
    PayGroup(String title, List<String> payList) {
        this.title = title;
        this.payList = payList;
    }
    
    public static PayGroup findByPayType(PayType payType) {
        return Arrays.stream(PayGroupAdvanced.values())
                .filter(payGroup -> payGroup.hasPayCode(payType))
                .findAny()
                .orElse(EMPTY);
    }
    
    public boolean hasPayCode(PayType payType) {
        return payList.stream()
                .anyMatch(pay -> pay == payType));
    }
}
@Test
public void PayGroup에게_직접_결제종류_물어보기_enum() throws Exception {
    PayType payType = selectPayType();
    PayGroupAdvanced payGroup = PayGroupAdvanced.findByPayType(payType);
    
    assertThat(payGroup.name(), is("BAEMIN_PAY"));
    assertThat(payGroup.getTitle(), is("배민페이"));
}

PayType으로 데이터를 받아 타입 안전성까지 확보하여 관련 처리를 진행할 수 있다.

4. 관리 주체를 DB에서 객체로

DB에서 코드를 관리할 경우 문제가 발생할 수 있다.

1. 코드명만 봐서는 무엇을 나타내는지 알 수 없다.

- 문서화가 되어있다 하더라도, 문서 업데이트가 잘 되어있는지 확신할 수 없어 DB를 다시 찾아봐야 하는 번거로움이 있다.

2. 항상 코드 테이블 조회 쿼리가 실행되어야 했다.

- 화면에 표시하기 위해 코드 테이블을 조회해야 한다.

 

Enum을 바로 JSON으로 리턴하게 되면, 상수 name만 출력된다.

Enum의 name과 title 모두 필요한 경우가 많다.

클래스의 생성자로 일관된 타입을 받기 위해 인터페이스를 하나 생성하자.

public interface EnumMapperType {
    String getCode();
    String getTitle();
}
@Getter
@ToString
public class EnumMapperValue {
    
    private String code;
    private String title;
    
    public EnumMapperValue(EnumMapperTupe enumMapperType) {
        code = enumMapperType.getCode();
        title = enumMapperType.getTitle();
    }  
}

Enum은 미리 선언한 인터페이스를 구현하면 된다.

public enum FeeType implements EnumMapperType {
    
    PERCENT("정율"),
    MONEY("정액");
    
    private String title;
    
    FeeType(String title) {
        this.title = title;
    }
    
    @Override
    public String getCode() {
        return name();
    }
    
    @Override
    public String getTitle() {
        return title;
    }
}

 

이제 Enum을 Value 클래스로 변경한 후, 전달하자.

@GetMapping("/no-bean-categories")
public List<EnumMapperValue> getNoBeanCategory() {
    return Arrays.stream(FeeType.values())
            .map(EnumMapperValue::new)
            .collect(Collectors.toList());
}

/*
[
  {
    "code": "PERCENT",
    "title": "정율"
  },
  {
    "code": "MONEY",
    "title": "정액"
  }
]

*/

 

 

 

 출처 

https://techblog.woowahan.com/2527/

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

 

728x90
반응형
blog image

Written by ner.o

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

반응형

JSON.parse()와 JSON.stringify()

- 자바스크립트에서 JSON 내장 객체를 이용하여 JSON 형식으로 표현된 데이터를 다루는 방법

JSON이란?

JSON은 Javascript Object Notation의 약자로, 데이터를 문자열의 형태로 나타내기 위해서 사용된다. 이름이 보여주듯 JSON은 본래 자바스크립트에서 파생되었지만 현재는 거의 표준으로 자리잡아 대부분의 언어에서 지원하는 데이터포맷이다. JSON은 특히 네트워크를 통해 서로 다른 시스템들이 데이터를 주고 받을 때 많이 사용되기 때문에 어렵지 않게 접할 수 있다.

JSON으로는 객체, 배열, 숫자, 문자열, 불린(boolean), 널(null)과 같은 다양한 데이터를 나타낼 수 있다.

JSON 내장 객체

자바스크립트에서는 JSON 포맷의 데이터를 간편하게 다룰 수 있도록 JSON 이라는 객체를 내장하고 있다. 이 객체는 자바스크립트 코드를 브라우저에서 실행하든 Node.js 런타임에서 실행하든 상관없이 전역(global)에서 접근이 가능합니다.

JSON 내장 객체는 Javascript 객체와 JSON 문자열 간의 상호 변환을 수행해주는 두 개의 메서드를 제공한다.

- JSON.parse()

- JSON.stringify()

JSON.parse(): JSON 문자열을 Javascript 객체로 변환

JSON 문자열을 Javascript 객체로 변환할 때는 JSON 객체의 parse() 메서드를 사용한다. parse() 메서드는 JSON 문자열을 인자로 받고 결과값으로 javascript 객체를 반환한다.

const str = `{
  "name": "홍길동",
  "age": 25,
  "married": false,
  "family": { "father": "홍판서", "mother": "춘섬" },
  "hobbies": ["독서", "도술"],
  "jobs": null
}`;

JSON.parse() 메서드에 str을 인자로 넘겨 호출해 결과값을 obj라는 변수에 저장한다.

const obj = JSON.parse(str);

obj 에 저장된 값을 콘솔에 출력해보면 JSON 문자열 형태의 데이터가 Javascript 객체의 형태로 변환되어 출력되는 것을 확인하자.

console.log(obj);

{
    name: "홍길동",
    age: 25,
    married: false,
    family: {
        father: "홍판서",
        mother: "춘섬"
    },
    hobbies: [
        "독서",
        "도술"
    ],
    jobs: null
}

JSON 문자열에서는 key 를 나타낼 때 반드시 쌍따옴표로 감싸주어야 하지만, Javascript 객체에서는 쌍따옴표를 꼭 사용할 필요가 없다. 

Javascript로 변환된 데이터는 .이나 [] 기호를 활용하여 각 속성에 접근할 수 있다.

> obj.name
< '홍길동'
> obj.age
< 25
> obj.married
< false
> obj.family
< {father: '홍판서', mother: '춘섬'}
> obj.family.mother
< '춘섬'
> obj.hobbies
< ['독서', '도술']
> obj.hobbies[1]
< '도술'
> obj.jobs
< null

외부에서 문자열의 형태로 주어진 데이터를 해당 언어에서 다루기 용이하도록 내장 데이터 타입으로 변환하는 과정을 역직렬화(deserialization)이라고 한다. 클라이언트에서 JSON 포맷으로 데이터를 보내면 서버에서 우선 Javascript 객체로 변환 후, 데이터를 처리한다.

JSON.stringify(): Javascript 객체를 JSON 문자열로 변환

역으로 Javascript 객체를 JSON 문자열로 변환할 때는 JSON 객체의 stringify() 메서드를 사용한다. stringify() 메서드는 Javascript 객체를 인자로 받고 JSON 문자열을 반환한다.

const obj = {
  name: "홍길동",
  age: 25,
  married: false,
  family: {
    father: "홍판서",
    mother: "춘섬",
  },
  hobbies: ["독서", "도술"],
  jobs: null,
}; // javascript 객체

JSON.stringify() 메서드에 obj 를 인자로 넘겨 호출해보자. 

const str = JSON.stringify(obj);

console.log(str);
'{"name":"홍길동","age":25,"married":false,"family":{"father":"홍판서","mother":"춘섬"},"hobbies":["독서","도술"],"jobs":null}'

str에 저장된 값을 콘솔로 출력해보면 Javascript 객체의 형태인 데이터가 JSON 형식의 문자열로 변환되어 출력되는 것을 확인할 수 있다.

 

stringify() 메서드의 3번째 인자로 들여쓰기 할 공백의 크기도 지정해줄 수 있다.

const str2 = JSON.stringify(obj, null, 2);
console.log(str2);

{
  "name": "홍길동",
  "age": 25,
  "married": false,
  "family": {
    "father": "홍판서",
    "mother": "춘섬"
  },
  "hobbies": [
    "독서",
    "도술"
  ],
  "jobs": null
}

당연하게도, JSON 형식의 문자열로 변환된 데이터는 더이상 . 이나 [] 기호를 사용하여 속성에 접근할 수 없다.

> str.name
< undefined

특정 언어의 내장 타입의 데이터를 외부에 전송하기 용이하도록 문자열로 변환하는 과정을 직렬화(serialization)이라고 한다. 

 

 출처 

https://www.daleseo.com/js-json/

 

JSON.parse()와 JSON.stringify()

Engineering Blog by Dale Seo

www.daleseo.com

 

728x90
반응형
blog image

Written by ner.o

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

반응형

MapStruct

  • Mapstruct란?
  • Dependency 설정
  • 사용법
    • Entity, Dto 클래스를 만들어보자.
    • Mapper를 만들자.
    • 빌드를 진행하자.

MapStruct란?

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.

MapStruct는 구성접근법에 대한 규약에 근거하여 Java Bean 종류 간의 매핑 구현을 크게 단순화한 code generator이다.

 

 참고  아래는 Java Mapping 프레임 워크 성능을 비교한 글이다.

https://www.baeldung.com/java-performance-mapping-frameworks

 

Performance of Java Mapping Frameworks | Baeldung

Compare the performance of the most popular Java mapping frameworks.

www.baeldung.com

 참고  아래는 ModelMapper 라이브러리에 대해 정리한 글이다.

https://frogand.tistory.com/180

 

[Java] ModelMapper 라이브러리

ModelMapper 라이브러리 의존성 추가 build.gradle implementation 'org.modelmapper:modelmapper:2.4.2' 변환 클래스들 정의 // 모든 클래스 Constructor, Getter, Setter 생략 class Address { String street; String city; } class Name { String

frogand.tistory.com

 

* MapStruct는 시간당 처리량이 높다.

 

Dependency 설정 (의존성 설정)

 참고  MapStruct 가이드

https://mapstruct.org/documentation/spring-extensions/reference/html/

 

MapStruct Spring Extensions 0.1.2 Reference Guide

MapStruct Spring Extensions is a Java annotation processor based on JSR 269 and as such can be used within command line builds (javac, Ant, Maven etc.) as well as from within your IDE. Also, you will need MapStruct itself (at least version 1.4.0.Final) in

mapstruct.org

1. maven 설정

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.2.Final</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2. gradle 설정

dependencies {
    implementation 'org.mapstruct:mapstruct:1.5.2.Final'
    annotationProcessor "org.mapstruct:mapstruct-processor:1.5.2.Final"
    annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
	
	compileOnly 'org.projectlombok:lombok:1.18.22'
    annotationProcessor 'org.projectlombok:lombok:1.18.22'
}

 

사용법

1. 예시를 위한 Entity와 Dto 클래스를 만들어보자.

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

import java.time.LocalDateTime;

@AllArgsConstructor
@Getter
@Builder
@EqualsAndHashCode
public class Order {

    private Long id;
    private String name;
    private String product;
    private Integer price;
    private String address;
    private LocalDateTime orderedTime;
}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@Builder
class OrderDto {

    private String name;
    private String product;
    private Integer price;
    private String address;
    private String img;
    private LocalDateTime orderedTime;
}

* OrderDto 클래스에는 Long id 필드가 존재하지 않고, String img 필드가 만들어졌다.

* 변환해서 저장하고자 하는 객체에는 Builder 혹은 모든 필드를 담을 수 있는 생성자가 있어야 한다.

 

2. 이를 매핑하는 OrderMapper를 만들어보자.

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper // (1)
public interface OrderMapper {
    OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class); // (2)

    @Mapping(target = "id", constant = "0L") // (3)
    Order convertOrderDtoToEntity(OrderDto orderDto);

    @Mapping(target = "img", expression = "java(order.getProduct() + \".jpg\")") // (4)
    OrderDto convertOrderToDto(Order order);
}

(1) @Mapper 어노테이션이 있어야 Mapstruct를 사용할 수 있다.

(2) 해당하는 Instance가 OrderMapper를 상속받아서 orderMapperImpl를 구현하게 될 것이다. 

(3) 일반적인 경우에 @Mapping 어노테이션을 붙이면 된다. Order 에는 id 필드가 존재하기 때문에 id 필드를 0L로 지정하겠다는 의미이다. (ignore 파라미터를 사용하면 0L로 지정하는 것이 아니라 해당 필드를 무시할 수 있다.)

(4) OrderDto에는 img 필드가 존재하기 때문에 expression을 기반으로 img 필드를 지정할 수 있다.

 

3. 빌드 진행 후, MapperImpl 클래스가 생성된다.

import java.time.LocalDateTime;
import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-11-04T15:46:35+0900",
    comments = "version: 1.5.2.Final, compiler: javac, environment: Java 1.8.0_311 (Oracle Corporation)"
)
public class OrderMapperImpl implements OrderMapper {

    @Override
    public Order orderDtoToEntity(OrderDto orderDto) {
        if ( orderDto == null ) {
            return null;
        }

        String name = null;
        String product = null;
        Integer price = null;
        String address = null;
        LocalDateTime orderedTime = null;

        name = orderDto.getName();
        product = orderDto.getProduct();
        price = orderDto.getPrice();
        address = orderDto.getAddress();
        orderedTime = orderDto.getOrderedTime();

        Long id = (long) 0L;

        Order order = new Order( id, name, product, price, address, orderedTime );

        return order;
    }

    @Override
    public OrderDto orderToDto(Order order) {
        if ( order == null ) {
            return null;
        }

        String name = null;
        String product = null;
        Integer price = null;
        String address = null;
        LocalDateTime orderedTime = null;

        name = order.getName();
        product = order.getProduct();
        price = order.getPrice();
        address = order.getAddress();
        orderedTime = order.getOrderedTime();

        String img = order.getProduct() + ".jpg";

        OrderDto orderDto = new OrderDto( name, product, price, address, img, orderedTime );

        return orderDto;
    }
}

 

해당 코드를 보면 나는 interface로 메소드만 만들어줬을 뿐인데, Mapper 클래스가 만들어진 것을 확인할 수 있다.

OrderMapper orderMapper = Mappers.getMapper(OrderMapper.class);
// .. 생략
Order order = orderMapper.convertOrderDtoToEntity(orderDto);

위와 같이 OrderMapper를 활용해서 변환작업을 쉽게 처리할 수 있다.

 

 출처 

https://huisam.tistory.com/entry/mapStruct

 

Spring Mapstruct - Java Entity DTO 매핑을 편하게 하자!

MapStruct? 안녕하세요~! ㅎㅎ 오늘은 Spring을 쓰면서 자주 쓰게 되는 라이브러리를 하나 소개할까 합니다! 바로 그것이 MapStruct 인데요! 이 Mapstruct란? MapStruct is a code generator that greatly simpli..

huisam.tistory.com

https://mapstruct.org/documentation/stable/reference/html/

 

MapStruct 1.5.2.Final Reference Guide

If set to true, MapStruct in which MapStruct logs its major decisions. Note, at the moment of writing in Maven, also showWarnings needs to be added due to a problem in the maven-compiler-plugin configuration.

mapstruct.org

 

728x90
반응형
blog image

Written by ner.o

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