void twoLetter(){
for (int ascii1 = 65; ascii1 < 91; ascii1++) {
for (int ascii2 = 65; ascii2 < 91; ascii2++) {
System.out.printf("%c %c \n",(char) ascii1, (char) ascii2);
}
}
}
문자열의 길이가 늘어나면 그 것만큼 for문을 작성해주어야 하는 문제점이 있다. 따라서 재귀 함수를 이용해서 문자열의 개수를 지정했을 때 그 길이만큼의 문자열들을 출력할 수 있는 메소드를 만들어보자
AAA, AAB, AAC, ...
ABA, ABB, ABC , ...
에서 앞 부분 2 글자는 고정되어있고 뒷 부분이 A, B, C, ... 로 바뀌는 것을 확인할 수 있다. 따라서 앞부분의 글자들을 prefix로 고정하고 뒷 부분의 글자들을 바꾸도록 할 수 있다.
void printThreeAlphabet(char letter1, String prefix){
if (letter1 == 'A'){
System.out.printf("%s%c\n", prefix,letter1);
} else if (letter1 > 'A') {
printThreeAlphabet((char)(letter1-1), prefix);
System.out.printf("%s%c\n", prefix,letter1);
}
}
public static void main(String[] args) {
PrintAlphabetWithRecursion printAlphabetWithRecursion = new PrintAlphabetWithRecursion();
printAlphabetWithRecursion.printThreeAlphabet('Z', "AA");
}
하지만 매번 prefix를 설정해주어야 하는 번거로움이 있다. 따라서 prefix를 업데이트 해줄 수 있는 부분을 재귀함수를 통해 구현하자
public static void printAlphabet(String prefix, int length) {
if (prefix.length() == length) {
// (2) 특정 길이가 되면 그 문자열을 출력한다.
System.out.println(prefix);
return;
}
for (char c = 'A'; c <= 'Z' ; c++) {
printAlphabet(prefix + c, length);
// (1) 특정 길이가 될 때까지 prefix + c를 이용해 prefix를 갱신해준다. (String + char -> String)
}
}
public static void main(String[] args) {
PrintAlphabetWithRecursion printAlphabetWithRecursion = new PrintAlphabetWithRecursion();
printAlphabet("", 3);
}
}
public class PrintAtoZCombination4 {
public static final String chars ="ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static void printAlphabet(String prefix, int depth) {
if (prefix.length() > depth) return;
System.out.println(prefix);
for (int i = 0; i < chars.length(); i++) {
printAlphabet(prefix + chars.charAt(i), depth);
}
}
public static void main(String[] args) {
printAlphabet("", 2);
}
}
가질 수 있는 모든 문자를 문자열(chars)로 나타내고 이를 이용해 prefix를 갱신하는 방식으로도 메소드를 작성할 수 있다.
(1) 클라이언트가 요청한 정보를 RequestDto에 담아서 Service 계층에서 DB에 있는 정보와 중복되는지 확인한다
(2-1) 중복되면 exception Manager를 통해 exception을 발생시킨 후 response의 error 메소드에 에러 코드를 담는다. 이 때 결과값은 null로 보내준다.
(3-1) controller를 통해 에러 코드를 클라이언트에 응답해준다.
(2-2) 중복되지 않으면 ResponseDto에 정보를 담는다
(3-2) ResponseDto에 담긴 정보를 "SUCCESS"라는 코드와 함께 클라이언트에 응답해준다.
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@PostMapping("/join")
public Response<UserJoinResponse> join(@RequestBody UserJoinRequest userJoinRequest){
log.info(userJoinRequest.getUserName(), userJoinRequest.getEmail());
UserDto userDto = userService.join(userJoinRequest);
return Response.success(new UserJoinResponse(userDto.getUserName(), userDto.getEmailAddress()));
}
}
응답 코드를 결과(Response Dto에 담겨진 정보 또는 null)와 함께 controller에 전달하기 위해 사용되는 클래스
@AllArgsConstructor
@Getter
public class Response<T> {
private String resultCode;
private T result;
// error가 발생하는 경우 작동하는 메소드
public static Response<Void> error(String resultCode){
return new Response(resultCode, null);
}
// 에러가 발생하지 않고 ResponseDto를 통해 결과가 넘어오는 경우
// 작동하는 메소드
public static <T> Response<T> success(T result) {
return new Response("SUCCESS", result);
}
}
이 때 @getter를 생략하면 HttpMediaTypeNotAcceptableException
가 발생한다.
관련 내용: https://velog.io/@quarara01/Resolved-org.springframework.web.HttpMediaTypeNotAcceptableException-Could-not-find-acceptable-representation
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUserName(String userName);
}
@AllArgsConstructor
@Getter
public enum ErrorCode {
DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "User name이 중복됩니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "internal server error가 발생했습니다."),
USER_NOT_FOUNDED(HttpStatus.NOT_FOUND, ""),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "");
private HttpStatus httpStatus;
private String message;
}
enum을 이용해 받은 수 있는 에러를 DUPLICATED_USER_NAME, INTERNAL_SERVER_ERROR, USER_NOT_FOUND, INVALID_PASSWORD 로 한정한다.
(1) Enum이란
상수들의 집합으로 상수가 많아져도 한 눈에 어떤 것이 있는지 파악할 수 있게 해준다. 또한 각각의 집합에서 같은 이름으로 정의된 상수가 있어도 컴파일 에러가 나지 않는다.
public class EnumExcercise {
enum Day{
MON, TUE, WED, THUR, FRI, SAT, SUN;
}
enum Month{
JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC;
}
enum Holiday{
SAT, SUN;
}
public static void main(String[] args) {
Day day = Day.SAT;
Holiday holiday = Holiday.SAT;
System.out.println("오늘은 " + day + "입니다");
System.out.println(holiday + "는 휴일입니다");
}
//오늘은 SAT입니다
//SAT는 휴일입니다
}
또한 생성자를 통해서 enum에 속성을 부여할 수 있고 getter 메소드를 이용해서 이 속성을 사용할 수 있다.
public class EnumExcercise {
enum Holiday{
SAT("서핑"), SUN("스케이트보드");
private String hobby;
Holiday(String hobby) {
this.hobby = hobby;
}
String getHobby(){
return hobby;
}
}
public static void main(String[] args) {
Holiday holiday = Holiday.SAT;
System.out.println(holiday + "는 휴일입니다");
System.out.println(holiday + "에는 " +holiday.getHobby() + "을 합니다.");
}
//SAT는 휴일입니다
//SAT에는 서핑을 합니다.
}
enum을 이용하면
@RestControllerAdvice
public class ExceptionManager {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> runtimeExceptionHander(RuntimeException e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Response.error(e.getMessage()));
}
@ExceptionHandler(HospitalReviewAppException.class)
public ResponseEntity<?> hospitalReviewAppExceptionHandler(HospitalReviewAppException e){
return ResponseEntity.status(e.getErrorCode().getHttpStatus())
.body(Response.error(e.getErrorCode().getMessage()));
// enum에서 정의한 속성을 getter를 이용해서 접근함
}
}
(1) 와일드카드
제네릭은 공불변이므로 A과 B의 하위 타입이어도 T< A >가 T< B >의 하위 타입이 아니다. 예를 들면 Integer가 Object의 하위 타입이지만 List< Integer >는 List< Object >의 하위 타입이 아닌 것이다.
@Test
void genericTest() {
List<Integer> list = Arrays.asList(1, 2, 3);
printCollection(list); // 컴파일 에러 발생
}
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
따라서 이러한 경우 정해지지 않은 unknown type인 와일드 카드를 이용해서 모든 타입을 받아줄 수 있도록 리스트를 선언할 수 있다.
@Test
void genericTest() {
List<Integer> list = Arrays.asList(1, 2, 3);
printCollection(list); // 에러가 발생하지 않음
}
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
하지만 와일드 카드로 선언된 타입은 unknown type이기 때문에 add를 할 때 문제가 생긴다. add로 값을 추가하려면 제네릭 타입 또는 그 자식을 넣어야 한다. List< Object >에는 Object 타입 또는 그 자식 타입인 Integer, String 등등을 넣을 수 있다. 하지만 와일드 카드는 unknown type이므로 어떤 타입을 대표하는지 알 수 없어 넣고자 하는 값이 자식 타입인지를 확인할 수 없다.
@Test
void genericTest() {
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 컴파일 에러
}
따라서 상한 범위와 하한 범위를 지정해서 호출 범위를 정해줄 수 있는데 이를 한정적 와일드카드라고 한다.
(2) 한정적 와일드 카드
class MyGrandParent {
}
class MyParent extends MyGrandParent {
}
class MyChild extends MyParent {
}
class AnotherChild extends MyParent {
}
MyGrandParent의 자식 클래스인 MyParent, 그 자식 클래스인 Mychild, AnotherChild가 있다고 하자.
i) 상한 경계 와일드 카드: 와일드 카드 타입에 extends를 사용해서 와일드 카드 타입의 상한 경계를 설정해주어 <? extends MyParent>에는 MyParents와 모든 MyParents의 자식 클래스가 올 수 있다. 따라서 우리는 MyParents와 MyParents의 자식 클래스 중에서 어떤 것이 올지 알 수 없으므로 아래의 경우에서 모두 컴파일 에러가 발생한다.
void addElement(Collection<? extends MyParent> c) {
c.add(new MyChild()); // 불가능(컴파일 에러)
c.add(new MyParent()); // 불가능(컴파일 에러)
c.add(new MyGrandParent()); // 불가능(컴파일 에러)
c.add(new Object()); // 불가능(컴파일 에러)
}
ii) 하한 경계 와일드 카드: super를 사용해 와일드카드의 하한 경계를 설정할 수 있다. 이 경우에는 MyParent와 임의의 MyParent의 부모 클래스가 모두 올 수 있다. 이 때 MyChild는 그 모든 클래스의 자식 클래스이고, MyParent는 자식 클래스 이거나 자신이므로 add가 가능하다. 반면 MyGrandParent는 MyParent의 임의의 부모 클래스가 올 수 있으므로 자기자신이 오는지를 특정할 수 없어 컴파일 에러가 발생한다.
void addElement(Collection<? super MyParent> c) {
c.add(new MyChild());
c.add(new MyParent());
c.add(new MyGrandParent()); // 불가능(컴파일 에러)
c.add(new Object()); // 불가능(컴파일 에러)
}
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
public UserDto join(UserJoinRequest request){
// userName이 중복되었으면 회원가입X -> Exception(예외) 발생
userRepository.findByUserName(request.getUserName())
.ifPresent(user-> {throw new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, String.format("UserName:%s", request.getUserName()));});
// save는 entity의 형태를 받음 -> entity로 변환해줌
User savedUser = userRepository.save(request.toEntity());
log.info(savedUser.getUserName());
return UserDto.builder()
.id(savedUser.getId())
.userName(savedUser.getUserName())
.emailAddress(savedUser.getEmailAddress())
.build();
}
}
Optional<User> userOptional = userRepository.findByUserName(request.getUserName());
if (userOptional.isPresent()){
throw new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, String.format("UserName:%s", request.getUserName()));
}
- ifPresent(): Void 타입을 return한다 - 값이 있으면 코드 실행, 값이 없으면 넘어감
userRepository.findByUserName(request.getUserName())
.ifPresent(user-> {throw new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, String.format("UserName:%s", request.getUserName()));});
@WebMvcTest
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName("회원가입 성공")
void join_success() throws Exception {
UserJoinRequest userJoinRequest = UserJoinRequest.builder()
.userName("abcde")
.password("1d5s3a")
.email("abcde@gmail.com")
.build();
when(userService.join(any())).thenReturn(mock(UserDto.class));
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userJoinRequest)))
.andDo(print())
.andExpect(status().isOk());
}
@Test
@DisplayName("회원가입 실패")
void join_failure() throws Exception {
UserJoinRequest userJoinRequest = UserJoinRequest.builder()
.userName("abcde")
.password("1d5s3a")
.email("abcde@gmail.com")
.build();
when(userService.join(any())).thenThrow(new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, ""));
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userJoinRequest)))
.andDo(print())
.andExpect(status().isConflict());
// enu에서
// DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "User name이 중복됩니다.") 로 정의했기 때문에
// isConflict로 예외가 발생했는지 확인
}
}