0830 - 0905
# 0830 - 0905
# 0831 - Spring : consumes์ produces์ ์ฐจ์ด
Controller์์ Mapping์ ํ ๋ ์ฃผ๊ณ ๋ฐ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ํจ์ผ๋ก์จ ์ค๋ฅ์ํฉ์ ์ค์ธ๋ค. ์ด๋ ์ฌ์ฉํ๋ ๊ฒ ์ค ํ๋๊ฐ Media Types์ด๋ค.
# consumes
consumes๋ ๋ค์ด์ค๋ ๋ฐ์ดํฐ ํ์ ์ ์ ์ํ ๋ ์ด์ฉํ๋ค.
@PostMapping(path = "/pets", consumes = MediaType.APPLICATION_JSON_VALUE)
public void addPet(@RequestBody Pet pet) {
// ...
}
- ์ด๋ ๊ฒ ์ฒ๋ฆฌ๋ฅผ ํ๊ฒ๋๋ฉด ํด๋น uri๋ฅผ ํธ์ถํ๋ ์ชฝ์์๋ ํค๋์ ๋ณด๋ด๋ ๋ฐ์ดํฐ๊ฐ json์ด๋ผ๋ ๊ฒ์ ๋ช ์ํด์ผ ํ๋ค.
Content-Type:application/java
# produces
produces๋ ๋ฐํํ๋ ๋ฐ์ดํฐ ํ์ ์ ์ ์ํ๋ค.
@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
- ์ด๋ด ๊ฒฝ์ฐ ๋ฐํ ํ์ ์ด json์ผ๋ก ๊ฐ์ ๋๋ค.
Accept:application/json
# ์์ฝ
- consumes๋ ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์๊ฒ ๋ณด๋ด๋ ๋ฐ์ดํฐ ํ์ ์ ๋ช ์ํ๋ค.
- produces๋ ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ๋ ๋ฐ์ดํฐ ํ์ ์ ๋ช ์ํ๋ค.
# 0901 - ๋ฐฐ์ด ์ ๋ ฌ
# Arrays.sort์ Comparator์ฌ์ฉ compare ๋ฉ์๋ ๊ตฌํ
- Comparator๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ์ฒด๋ฅผ ๋น๊ตํ ์ ์๋๋ก ํด์ฃผ๋ ์ธํฐํ์ด์ค์ด๋ค.
- ์๋ฐ ๊ธฐ๋ณธ์๋ฃํ์ด ์๋ ์ฌ์ฉ์ ํด๋์ค์ ๋น๊ต๋ ํน์ ๊ท์น์ ์ํด ๋น๊ต๋ฅผ ํ๊ณ ์ถ์ ๊ฒฝ์ฐ์ ๊ตฌํํ๋ค.
# Arrays.sort์ ํํ
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
- ์ธ์๋ก Comparator์ ํ์ ์ ๋ฃ๊ณ compare ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํ๋ค.
# ์์
String[] arr = new String[N]; // ๋ฐฐ์ด์ ๋จ์ด๊ฐ ์ด๋ฏธ ์ด๊ธฐํ ๋์๋ค๊ณ ๊ฐ์
Arrays.sort(arr, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
// ๋จ์ด ๊ธธ์ด๊ฐ ๊ฐ์ ๊ฒฝ์ฐ
if(s1.length() == s2.length()} {
return s1.compareTo(s2); // ์ฌ์ ์ ์ ๋ ฌ
}
// ๊ทธ ์ธ์ ๊ฒฝ์ฐ
else {
return s1.length() - s2.length();
}
}
});
- compare ๋ฉ์๋ฅด ๋ฆฌํด ํ์
์ด int์ธ ์ด์ ๋
์์ ์ ์
,0
,์์ ์ ์
์ ๋ฐ๋ผ ์์น๋ฅผ ๋ณ๊ฒฝํ๋ค.- ์์ = ์์น ๋ณ๊ฒฝ
- 0 and ์์ = ๊ทธ๋๋ก
# 0902 - ์๋ฐ ์ ๊ท ํํ์ (Pattern, Matcher)
# ์ ๊ทํํ์
์ ๊ทํํ์(Regular Expression)์ด๋ ์ปดํจํฐ ๊ณผํ์ ์ ๊ท์ธ์ด๋ก๋ถํฐ ์ ๋ํ ๊ฒ์ผ๋ก ํน์ ํ ๊ท์น์ ๊ฐ์ง ๋ฌธ์์ด์ ์งํฉ์ ํํํ๊ธฐ ์ํด ์ฐ์ด๋ ํ์์ธ์ด์ด๋ค. ์ ํด์ง ํ์์ ๋ง๋์ง ๊ฒ์ฆํด์ผํ ๋ ์ฌ์ฉ ํ ์ ์๋ค.
# ์๋ฐ์์์ ์ ๊ทํํ์
- ์ ๊ท ํํ์์ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์๋ฐ API java.util.regex ํจํค์ง๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
# Pattern ํด๋์ค
import java.util.regex.Pattern;
public class RegexExample {
public static void main(String[] args) {
String pattern = "^[0-9]*$"; //์ซ์๋ง
String val = "123456789"; //๋์๋ฌธ์์ด
boolean regex = Pattern.matches(pattern, val);
System.out.println(regex);
}
}
- ๋ฌธ์์ด ํจํด ๊ฒ์ฆ์, Pattern ํด๋์ค์ matches() ๋ฉ์๋๋ฅผ ํ์ฉ.
# Pattern ํด๋์ค์ ์ฃผ์ ๋ฉ์๋
- compile(String regex) : ์ฃผ์ด์ง ์ ๊ทํฌํ์์ผ๋ก๋ถํฐ ํจํด์ ์์ฑ
- matcher(CharSequence input) : ๋์ ๋ฌธ์์ด์ด ํจํด๊ณผ ์ผ์นํ ๊ฒฝ์ฐ true๋ฅผ ๋ฐํ.
- asPredicate() : ๋ฌธ์์ด์ ์ผ์น์ํค๋๋ฐ ์ฌ์ฉํ ์ ์๋ ์ ์ด๋ฅผ ์์ฑ.
- pattern() : ์ปดํ์ผ๋ ์ ๊ทํํ์์ String ํํ๋ก ๋ฐํ.
- split(CharSequence input) : ๋ฌธ์์ด์ ์ฃผ์ด์ง ์ธ์๊ฐ CharSequence ํจํด๋ฐ ๋ฐ๋ผ ๋ถ๋ฆฌ.
# Matcher ํด๋์ค
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExample {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("^[a-zA-Z]*$"); //์๋ฌธ์๋ง
String val = "abcdef"; //๋์๋ฌธ์์ด
Matcher matcher = pattern.matcher(val);
System.out.println(matcher.find());
}
}
- Matcher ํด๋์ค๋ ๋์ ๋ฌธ์์ด์ ํจํด์ ํด์ํ๊ณ ์ฃผ์ด์ง ํจํด๊ณผ ์ผ์นํ๋์ง ํ๋ณํ ๋ ์ฃผ๋ก ์ฌ์ฉ.
# Matcher ํด๋์ค ์ฃผ์ ๋ฉ์๋
- matches() : ๋์ ๋ฌธ์์ด๊ณผ ํจํด์ด ์ผ์นํ ๊ฒฝ์ฐ true ๋ฐํ.
- find() : ๋์ ๋ฌธ์์ด๊ณผ ํจํด์ด ์ผ์นํ๋ ๊ฒฝ์ฐ true๋ฅผ ๋ฐํํ๊ณ , ๊ทธ ์์น๋ก ์ด๋.
- find(int start) : start์์น ์ดํ๋ถํฐ ๋งค์นญ๊ฒ์์ ์ํ.
- start() : ๋งค์นญ๋๋ ๋ฌธ์์ด ์์์์น ๋ฐํ.
- start(int group) : ์ง์ ๋ ๊ทธ๋ฃน์ด ๋งค์นญ๋๋ ์์์์น ๋ฐํ.
- end() : ๋งค์นญ๋๋ ๋ฌธ์์ด ๋ ๋ค์ ๋ฌธ์์์น ๋ฐํ.
- end(int group) : ์ง์ ๋ ๊ทธ๋ฃน์ด ๋งค์นญ๋๋ ๋ ๋ค์ ๋ฌธ์์์น ๋ฐํ.
- group() : ๋งค์นญ๋ ๋ถ๋ถ์ ๋ฐํ.
- group(int group) : ๋งค์นญ๋ ๋ถ๋ถ์ค group๋ฒ ๊ทธ๋ฃนํ ๋งค์นญ๋ถ๋ถ ๋ฐํ.
- groupCount() : ํจํด๋ด ๊ทธ๋ฃนํํ(๊ดํธ์ง์ ) ์ ์ฒด ๊ฐฏ์๋ฅผ ๋ฐํ.
# 0903 - Bean Validation ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ํ๋ ์์ํฌ ์ฌ์ฉํ๊ธฐ.
# ์ค์น
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
# ์ ์ฝ ์ค์ ๊ณผ ๊ฒ์ฌ
public class CreateContact {
@Length(max = 64) // ์ต๋ ๊ธธ์ด 64
@NotBlank // ๋น๋ฌธ์์ด์ ์๋จ
private String uid;
@NotNull // null ์๋จ
private ContactType contactType;
@Length(max = 1_600) // ์ต๋ ๊ธธ์ด 1,600
private String contact;
}
- ๋๋ฉ์ธ ๊ฒ์ฆ
@BeforeClass
public static void beforeClass() {
Locale.setDefault(Locale.US); // locale ์ค์ ์ ๋ฐ๋ผ ์๋ฌ ๋ฉ์์ง๊ฐ ๋ฌ๋ผ์ง๋ค.
}
@Test
public void test_validate() {
// Given
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
final CreateContact createContact = CreateContact
.builder()
.uid(null) // @NotBlank๊ฐ ์ ์๋์ด ์๊ธฐ๋๋ฌธ์ null์ด ์ค๋ฉด ์๋๋ค.
.contact("000")
.contactType(ContactType.PHONE_NUMBER)
.build();
// When
final Collection<ConstraintViolation<CreateContact>> constraintViolations = validator.validate(createContact);
// Then
assertEquals(1, constraintViolations.size()); // ConstraintViolation์ ์คํจ์ ๋ํ ์ ๋ณด๊ฐ ๋ด๊ธด๋ค.
assertEquals("must not be blank", constraintViolations.iterator().next().getMessage());
}
- ์ ํจ์ฑ ๊ฒ์ฌ ๊ฒ์ฆ ์ฝ๋
# Spring์์ ์ฌ์ฉํ๊ธฐ
- Service๋ Bean์์ ์ฌ์ฉํ๊ธฐ ์ํด์๋
@Validated
์@Valid
๋ฅผ ์ถ๊ฐ
@Validated // ์ฌ๊ธฐ์ ์ถ๊ฐ
@Service
public class ContactService {
public void createContact(@Valid CreateContact createContact) { // '@Valid'๊ฐ ์ค์ ๋ ๋ฉ์๋๊ฐ ํธ์ถ๋ ๋ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์งํํ๋ค.
// Do Something
}
}
- Controller์์๋
@Validated
๊ฐ ํ์ ์๋ค. ๊ฒ์ฌ๋ฅผ ์งํํ ๊ณณ์ '@Valid'๋ฅผ ์ถ๊ฐํ๋ฉด ๋๋ค.
@PostMapping("/contacts")
public Response createContact(@Valid CreateContact createContact) { // ๋ฉ์๋ ํธ์ถ ์ ์ ํจ์ฑ ๊ฒ์ฌ ์งํ
return Response
.builder()
.header(Header
.builder()
.isSuccessful(true)
.resultCode(0)
.resultMessage("success")
.build())
.build();
}
# ์ปจํ ์ด๋์์์ ์ฌ์ฉ(์ปฌ๋ ์ , ๋งต, ...)
public class DeleteContacts {
@Min(1)
private Collection<@Length(max = 64) @NotBlank String> uids;
}
# ์ฌ์ฉ์ ์ ์ ์ ์ฝ(Custom Constraint)
- ์์์ ์ ์ฝ(Constraint)๊ณผ ๊ฒ์ฆ์(Validator)๋ฅผ ๊ตฌํ
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = NoEmojiValidator.class)
@Documented
public @interface NoEmoji{
String message() default "Emoji is not allowed";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List{
NoEmoji[] value();
}
public class NoEmojiValidator implements ConstraintValidator<NoEmoji, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isEmpty(value) == true) {
return true;
}
return EmojiParser.parseToAliases(value).equals(value);
}
}
}
public class CreateContact {
@NoEmoji
@Length(max = 64)
@NotBlank
private String uid;
@NotNull
private ContactType contactType;
@Length(max = 1_600)
private String contact;
}
# ์ ์ฝ ๊ทธ๋ฃน(Grouping)
public class Message {
@Length(max = 128)
@NotEmpty
private String title;
@Length(max = 1024)
@NotEmpty
private String body;
@Length(max = 32, groups = Ad.class)
@NotEmpty(groups = Ad.class) // ๊ทธ๋ฃน์ ์ง์ ํ ์ ์๋ค. (๊ธฐ๋ณธ ๊ฐ์ javax.validation.groups.Default)
private String contact;
@Length(max = 64, groups = Ad.class)
@NotEmpty(groups = Ad.class)
private String removeGuide;
}
public interface Ad {
}
- 'Ad.class'๋ ๋จ์ํ ๊ทธ๋ฃน์ ์ง์ ํ๊ธฐ ์ํ ๋ง์ปค ์ธํฐํ์ด์ค(Marker Interface)๋ค.
@Validated
@Service
public class MessageService {
@Validated(Ad.class) // ๋ฉ์๋ ํธ์ถ ์ Ad ๊ทธ๋ฃน์ด ์ง์ ๋ ์ ์ฝ๋ง ๊ฒ์ฌํ๋ค.
public void sendAdMessage(@Valid Message message) {
// Do Something
}
public void sendNormalMessage(@Valid Message message) {
// Do Something
}
/**
* ์ฃผ์: ์ด๋ ๊ฒ ํธ์ถํ๋ฉด Spring AOP Proxy ๊ตฌ์กฐ์ @Valid๋ฅผ ์ค์ ํ ๋ฉ์๋๊ฐ ํธ์ถ๋์ด๋ ์ ํจ์ฑ ๊ฒ์ฌ๊ฐ ๋์ํ์ง ์๋๋ค.
* Spring์ AOP Proxy ๊ตฌ์กฐ์ ๋ํ ์ค๋ช
์ ๋ค์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ์.
* - https://docs.spring.io/spring/docs/5.2.3.RELEASE/spring-framework-reference/core.html#aop-understanding-aop-proxies
*/
public void sendMessage(Message message, boolean isAd) {
if (isAd) {
sendAdMessage(message);
} else {
sendNormalMessage(message);
}
}
# ํด๋์ค ๋จ์ ์ ์ฝ(Class Level Constraint)๊ณผ ์กฐ๊ฑด๋ถ ๊ฒ์ฌ(Conditional Validation)
- ๋๋ฉ์ผ ๋ณด๋ธ์ ์์ฑ๊ธ ๊ฐ์ ๋ฐ๋ผ ๋ฐ์ดํฐ ์ ํ์ฑ ๊ฒ์ฌ๋ฅผ ๋ค๋ฅด๊ฒ ํด์ผํ ๊ฒฝ์ฐ์ ์ฌ์ฉ
@AdMessageConstraint // ์ด ์ปค์คํ
์ ์ฝ์ ๊ตฌํํ ๊ฒ์ด๋ค.
public class Message {
@Length(max = 128)
@NotEmpty
private String title;
@Length(max = 1024)
@NotEmpty
private String body;
@Length(max = 32, groups = Ad.class)
@NotEmpty(groups = Ad.class)
private String contact;
@Length(max = 64, groups = Ad.class)
@NotEmpty(groups = Ad.class)
private String removeGuide;
private boolean isAd; // ๊ด๊ณ ์ฌ๋ถ๋ฅผ ์ค์ ํ ์ ์๋ ์์ฑ
}
@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = AdMessageConstraintValidator.class)
@Documented
public @interface AdMessageConstraint {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
public class AdMessageConstraintValidator implements ConstraintValidator<AdMessageConstraint, Message> {
private Validator validator;
public AdMessageConstraintValidator(Validator validator) {
this.validator = validator;
}
@Override
public boolean isValid(Message value, ConstraintValidatorContext context) {
if (value.isAd()) {
final Set<ConstraintViolation<Object>> constraintViolations = validator.validate(value, Ad.class);
if (CollectionUtils.isNotEmpty(constraintViolations)) {
context.disableDefaultConstraintViolation();
constraintViolations
.stream()
.forEach(constraintViolation -> {
context.buildConstraintViolationWithTemplate(constraintViolation.getMessageTemplate())
.addPropertyNode(constraintViolation.getPropertyPath().toString())
.addConstraintViolation();
});
return false;
}
}
return true;
}
}
}
@Validated
@Service
public class MessageService {
/**
* message.isAd๊ฐ true์ด๋ฉด contcat, removeGuide ์์ฑ๊น์ง ๊ฒ์ฌํ๋ค.
*/
public void sendMessage(@Valid Message message) {
// Do Something
}
# ์ค๋ฅ ์ฒ๋ฆฌ(Error Handling)
- ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ์ ์คํจ๊ฐ ๋ฐ์ํ๋ฉด ConstraintViolationException ๋ฐ์. ์ด๋ฅผ @ControllerAdvice ๊ตฌํ ์ปจํธ๋กค๋ฌ์์ @ExceptionHandler ํธ๋ค๋ฌ ๊ตฌํ
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = ConstraintViolationException.class) // ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ ์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ์ฒ๋ฆฌ
@ResponseBody
protected Response handleException(ConstraintViolationException exception) {
return Response
.builder()
.header(Header
.builder()
.isSuccessful(false)
.resultCode(-400)
.resultMessage(getResultMessage(exception.getConstraintViolations().iterator())) // ์ค๋ฅ ์๋ต์ ์์ฑ
.build())
.build();
}
protected String getResultMessage(final Iterator<ConstraintViolation<?>> violationIterator) {
final StringBuilder resultMessageBuilder = new StringBuilder();
while (violationIterator.hasNext() == true) {
final ConstraintViolation<?> constraintViolation = violationIterator.next();
resultMessageBuilder
.append("['")
.append(getPopertyName(constraintViolation.getPropertyPath().toString())) // ์ ํจ์ฑ ๊ฒ์ฌ๊ฐ ์คํจํ ์์ฑ
.append("' is '")
.append(constraintViolation.getInvalidValue()) // ์ ํจํ์ง ์์ ๊ฐ
.append("'. ")
.append(constraintViolation.getMessage()) // ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ ์ ๋ฉ์์ง
.append("]");
if (violationIterator.hasNext() == true) {
resultMessageBuilder.append(", ");
}
}
return resultMessageBuilder.toString();
}
protected String getPopertyName(String propertyPath) {
return propertyPath.substring(propertyPath.lastIndexOf('.') + 1); // ์ ์ฒด ์์ฑ ๊ฒฝ๋ก์์ ์์ฑ ์ด๋ฆ๋ง ๊ฐ์ ธ์จ๋ค.
}
}
# ๋์ ๋ฉ์์ง ์์ฑ(Message Interpolation)
...
public @interface NoEmoji{
String message() default "Emoji is not allowed";
...
- ๋งค๊ฐ ๋ณ์๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
- '{}'๋๋ '${}'๋ก ๋๋ฌ์ผ๋ค.
- {,},,$๋ ๋ฌธ์๋ก ์ทจ๊ธ.
- '{'๋ ๋งค๊ฐ๋ณ์์ ์์, '}'๋ ๋งค๊ฐ๋ณ์์ ๋, \๋ ํ์ฅ๋ฌธ์, '$'๋ ํํ์ ์์์ผ๋ก ์ทจ๊ธ.
# ์ฐธ๊ณ
https://meetup.toast.com/posts/223