배열 요소 중 3개를 뽑아 곱했을 때 나올 수 있는 최대값을 구하라.
public int largestProductOfThree(int[] arr) { // 배열 요소 중 3개를 뽑아 곱했을 때 나올 수 있는 최대값을 구하라
int[] arr2 = arr.clone();
int[] arr3 = arr.clone();
int result = -1;
for(int j=0; j<arr3.length; j++) { // 절대값 큰 순서대로 정렬
int max = 0;
int index = 0;
for (int i = 0; i < arr2.length; i++) {
if (Math.abs(arr2[i]) > max) {
index = i;
max = Math.abs(arr2[i]);
}
}
arr3[j] = arr2[index];
arr2[index]=0;
}
boolean containPositive = false;
for(int i = 0; i < arr3.length; i++){
if(arr3[i]>0) {
containPositive = true;
break;
}
}
if(!containPositive) { //arr3가 전부 음수면
result = arr3[arr3.length-1]*arr3[arr3.length-2]*arr3[arr3.length-3];
return result;
}
for(int j=0; j<=arr3.length-3; j++){
result = arr3[0]*arr3[1]*arr3[2+j];
if(result>0) break;
}
if(result<0){
for(int k=2; k<=arr3.length-2; k++){
result = arr3[0]*arr3[1+k]*arr3[2];
if(result>0) break;
}
}
if(result<0){
for(int i=3; i<=arr3.length-2; i++){
result = arr3[0+i]*arr3[1]*arr3[2];
if(result>0) break;
}
}
return result;
}
모든테스트 통과
public int largestProductOfThree2(int[] arr) { // 간단하게 압축 가능
//배열을 오름차순으로 정리합니다.
Arrays.sort(arr);
int arrLength = arr.length;
//가장 큰 양수 3가지를 곱한 값
int candidate1 = arr[arrLength - 1] * arr[arrLength - 2] * arr[arrLength - 3];
//가장 작은 음수 2가지와, 가장 큰 양수를 곱한 값
int candidate2 = arr[arrLength - 1] * arr[0] * arr[1];
return Math.max(candidate1, candidate2);
}
이렇게 훨씬 간단하게도 가능하다.
엔터프라이즈 애플리케이션 아키텍처 패턴의 하나
- 요청 데이터를 하나의 객체로 전달 받는 역할을 해줌
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
return new ResponseEntity<Map>(map, HttpStatus.CREATED);
}
...
...
}
기존에 사용하던 방식은 postMember()
에 파라미터로 추가되는 @RequestParam
의 개수가 계속해서 늘어난다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(MemberDto memberDto) {
return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
}
...
...
}
DTO 클래스를 적용하면 위와 같이 간단하게 가능
마찬가지로 필요한 데이터를 전달 받기 위해 데이터를 검증하는 "유효성(Validation)검증" 도 MemberDto 클래스에 작성해 기능분리가 가능해진다.
public class MemberDto {
@Email // email양식에 맞는지 검사해주는 애너테이션
private String email;
private String name;
private String phone;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
@RequestParam
쪽 코드를 DTO 클래스의 객체로 수정@RequestBody
@ResponseBody
Spring MVC에서는 핸들러 메서드에 @ResponseBody
애너테이션이 붙거나 핸들러 메서드의 리턴 값이 ResponseEntity
일 경우, 내부적으로 HttpMessageConverter
가 동작하게 되어 응답 객체(여기서는 DTO 클래스의 객체)를 JSON 형식으로 바꿔준다.
역직렬화(Deserialization)
요청 받은 JSON 형식의 데이터를 DTO 같은 Java의 객체로 변환하는 것
직렬화(Serialization)
응답데이터를 전송하기 위해 DTO 같은 Java의 객체를 JSON 형식으로 변환하는 것
@RequestBody
애너테이션을 붙여야 한다.1차적으로 프론트엔드 쪽에서 유효성 검사를 진행하지만 자바스크립트로 전송되는 데이터는 브라우저의 개발자 도구를 사용해서 브레이크포인트(breakpoint)를 추가한 뒤에 얼마든지 그 값을 조작할 수 있기 때문에 서버 쪽에서 한번 더 유효성 검사를 진행해야 된다.
DTO 클래스에 유효성 검증을 적용하기 위해서는 Spring Boot에서 지원하는 Starter가 필요하다.
build.gradle 파일의 dependencies 항목에 'org.springframework.boot:spring-boot-starter-validation’
을 추가해야 한다.
@NotBlank(message = "~~~")
@NotBlank
만 쓰면 유효성 검증에 실패했을 때 에러 메시지가 콘솔에 출력@Email
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "~~~")
@Valid
@RequestBody
앞)에 사용@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
...
...
}
@Pattern()
@Pattern(regexp = "^\\S+(\\s?\\S+)*$", message = "~~~")
정규 표현식(Reqular Experssion)
@Min(1)
@Validated
@RequestMapping
뒤)에 사용DTO 클래스의 유효성 검증을 위해서 사용한 위 기능들은 Jakarta Bean Validation이라는 유효성 검증을 위한 표준 스펙에서 지원하는 내장 애너테이션들이다.
Jakarta Bean Validation
- 라이브러리처럼 사용할 수 있는 API가 아닌 스펙(사양, Specification) 자체다. ( 일종의 기능 명세를 의미 )
- Java Bean 스펙을 준수하는 Java 클래스라면 Jakarta Bean Validation의 애너테이션을 사용해서 유효성 검증을 할 수 있다.
Jakarta Bean Validation에 내장된(Built-in) 애너테이션 외에도 필요한 기능이 있다면 직접 애너테이션을 정의해 사용할 수 있다.
Custom Validator를 구현하기 위한 절차
NotSpace 인터페이스
- 공백을 허용하지 않는 Custom Annotation
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {NotSpaceValidator.class}) // (1)
public @interface NotSpace {
String message() default "공백이 아니어야 합니다"; // (2)
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Target, @Retention는 Section1내용 참고
@Target
= 애너테이션을 적용할 “대상"을 지정하는 데 사용
@Retention
= 특정 애너테이션의 지속 시간을 결정하는 데 사용
@Constraint
(제약조건)
@NotSpace
애너테이션이 멤버 변수에 추가되었을 때, 동작 할 Custom Validator를 연결해주는 용도로 사용했다고 생각하면 된다.NotSpaceValidator 클래스
- Custom Validator 구현
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class NotSpaceValidator implements ConstraintValidator<NotSpace, String> {
@Override
public void initialize(NotSpace constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || StringUtils.hasText(value);
}
}
<NotSpace, String>
제네릭에서 NotSpace는 CustomValidator와 매핑된 Custom Annotation(@NotSpace
)을 의미하며, String은 Custom Annotation으로 검증할 대상 멤버 변수의 타입을 의미
CoffeeController 클래스
import com.codestates.coffee.CoffeePatchDto;
import com.codestates.coffee.CoffeePostDto;
import org.springframework.http.HttpStatus;
//import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
//import javax.validation.constraints.Min;
import javax.validation.constraints.Positive;
//import java.util.HashMap;
//import java.util.Map;
//import java.util.Objects;
@RestController
@RequestMapping("/v1/coffees")
@Validated // @Min() 등을 처리해주는 것
public class CoffeeController {
@PostMapping
public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto){
return new ResponseEntity<>(coffeePostDto, HttpStatus.CREATED);
}
@PatchMapping("/{coffee-id}")
public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
@Valid @RequestBody CoffeePatchDto coffeePatchDto){
coffeePatchDto.setCoffeeId(coffeeId);
return new ResponseEntity(coffeePatchDto, HttpStatus.OK);
}
@GetMapping("/{coffee-id}") // 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션
public ResponseEntity getCoffee(@PathVariable("coffee-id")long coffeeId){ // 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드
System.out.println("# coffeeId: " + coffeeId);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() { // 회원 목록을 클라이언트에게 제공하는 핸들러 메서드
System.out.println("# get coffees");
return new ResponseEntity<>(HttpStatus.OK);
}
}
@Valid와 @Validated의 차이점에 대해 의문이 생겨 추가로 블로깅했다.
CoffeePostDto 클래스
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class CoffeePostDto {
@NotBlank
private String korName;
@NotBlank
@Pattern(regexp = "^([a-zA-Z]+\\s?[a-zA-Z])+$") // 영어만 허용
// [A]* : A가 0개 이상이다.
// [A]+ : A가 1개 이상이다.
// \\s? : 공백이 0개 or 1개다.
// 즉, "^([a-zA-Z]+\\s?[a-zA-Z])*$"는 알파벳(몇개인지 상관x) + 공백 + 알파벳(몇개인지 상관x)이다.
// n\d* : n 뒤에 숫자가 0개 이상이라는 의미. “n”, “n1”, “n123” 에 모두 매치된다.
private String engName;
@Range(min = 100, max= 50000)
private Integer price;
public String getKorName() {
return korName;
}
public void setKorName(String korName) {
this.korName = korName;
}
public String getEngName() {
return engName;
}
public void setEngName(String engName) {
this.engName = engName;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
CoffeePostDto 클래스에서 영어만 허용하도록 @Pattern
을 사용하는 과정에서 정규표현식 때문에 고생 좀 했다.
CoffeePatchDto 클래스
package com.codestates.coffee;
import com.codestates.member.NotSpace;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Pattern;
public class CoffeePatchDto {
private long coffeeId;
@NotSpace // null가능하게 해줘야 함
// @Pattern(regexp ="^\\S+(\\s?\\S+)*$") // << 이거로 대신 써도 됨
private String korName;
@Pattern(regexp = "^([a-zA-Z]+\\s?[a-zA-Z])*$")
private String engName;
@Range(min = 100, max= 50000)
private Integer price; // int로 하면 null이 안들어간다. 기본값인 0이 들어가기 때문에 range에 걸려서 선택적으로 사용 못함
public long getCoffeeId() {
return coffeeId;
}
public void setCoffeeId(long coffeeId) {
this.coffeeId = coffeeId;
}
public String getKorName() {
return korName;
}
public void setKorName(String korName) {
this.korName = korName;
}
public String getEngName() {
return engName;
}
public void setEngName(String engName) {
this.engName = engName;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
}
price변수 유효성 검사에서 null을 수용할 수 잇게 int 대신 Integer를 써야 했다.
^ | 문자열의 시작 |
$ | 문자열의 끝 |
. | 임의의 한 문자 |
* | 문자가 0번 이상 발생 |
+ | 문자가 1번 이상 발생 |
? | 문자가 0번 혹은 1번 발생 |
[ ] | 문자의 집합 범위를 나타냄 [0-9] : 숫자 (0부터 9) [a-z] : 알파벳 (a부터 z) 앞에 ^가 나타나면 not을 의미 |
{ } | 횟수 또는 범위를 의미 |
( ) | 소괄호 안의 문자를 하나의 문자로 인식 |
| | or 조건 |
\ | 확장 문자의 시작 |
\b | 단어의 경계 |
\B | 단어가 아닌 것의 경계 |
\A | 입력의 시작부분 |
\G | 이전 매치의 끝 |
\Z | 입력의 끝이지만 종결자가 있는 경우 |
\z | 입력의 끝 |
\s | 공백문자 |
\S | 공백문자가 아닌 나머지 문자 |
\w | 알파벳이나 숫자 |
\W | 알파벳이나 숫자를 제외한 문자 |
\d | [0-9]와 동일 |
\D | 숫자를 제외한 모든 문자 |
Anotation | 제약조건 |
@NotNull | Null 불가 |
@Null | Null만 입력 가능 |
@NotEmpty | Null, 빈 문자열 불가 |
@NotBlank | Null, 빈 문자열, 스페이스만 있는 문자열 불가 |
@Size(min=,max=) | 문자열, 배열등의 크기가 만족하는가? |
@Pattern(regex=) | 정규식을 만족하는가? |
@Max(숫자) | 지정 값 이하인가? |
@Min(숫자) | 지정 값 이상인가 |
@Future | 현재 보다 미래인가? |
@Past | 현재 보다 과거인가? |
@Positive | 양수만 가능 |
@PositiveOrZero | 양수와 0만 가능 |
@Negative | 음수만 가능 |
@NegativeOrZero | 음수와 0만 가능 |
이메일 형식만 가능 | |
@Digits(integer=, fraction = ) | 대상 수가 지정된 정수와 소수 자리 수 보다 작은가? |
@DecimalMax(value=) | 지정된 값(실수) 이하인가? |
@DecimalMin(value=) | 지정된 값(실수) 이상인가? |
@AssertFalse | false 인가? |
@AssertTrue | true 인가? |