2022.11.28 (월요일)

yeonicerely·2022년 11월 28일
0

1. 알고리즘: 문자열 조합 만들기

A. 2개의 문자열을 반복문을 이용해서 출력하기

    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문을 작성해주어야 하는 문제점이 있다. 따라서 재귀 함수를 이용해서 문자열의 개수를 지정했을 때 그 길이만큼의 문자열들을 출력할 수 있는 메소드를 만들어보자

B. 3개의 문자열을 재귀를 이용하여 출력하기

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를 업데이트 해줄 수 있는 부분을 재귀함수를 통해 구현하자

C. n개의 문자열을 재귀함수를 이용해 출력하기

    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를 갱신하는 방식으로도 메소드를 작성할 수 있다.

2. Spring Boot를 이용해 로그인 기능 만들기 (1)

A. 뼈대 만들기

(1) 클라이언트가 요청한 정보를 RequestDto에 담아서 Service 계층에서 DB에 있는 정보와 중복되는지 확인한다

(2-1) 중복되면 exception Manager를 통해 exception을 발생시킨 후 response의 error 메소드에 에러 코드를 담는다. 이 때 결과값은 null로 보내준다.
(3-1) controller를 통해 에러 코드를 클라이언트에 응답해준다.

(2-2) 중복되지 않으면 ResponseDto에 정보를 담는다
(3-2) ResponseDto에 담긴 정보를 "SUCCESS"라는 코드와 함께 클라이언트에 응답해준다.

a) controller

@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()));

    }
}

b) Response 클래스

응답 코드를 결과(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

B. 아이디 중복 체크하기 + 예외 처리하기

c) Repository

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUserName(String userName);
}

d) ErrorCode

@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을 이용하면

  • 허용되는 값들을 제한할 수 있다: 요일은 월, 화, 수, 목, 금, 토, 일로 제한
  • 데이터의 그룹을 관리할 수 있다.: 요일/달/휴일과 같이 데이터를 그룹으로 만들어 어느 값이 어디에 포함되는지 쉽게 알 수 있다.
  • 리팩토링 시 바꾸어야할 범위가 최소화된다: 내용을 추가해야하면 enum 코드만 수정하면 된다
  • 데이터들 사이의 연관관계를 표현할 수 있다.: SAT와 "서핑"과 같이 연관관계를 표현할 수 있다.

e) Exception Manager

@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를 이용해서 접근함
    }
}
  • < ? >
    : < ? extends Object > 의 줄임 표현으로 어떤 자료형의 객체도 매개변수로 받겠다는 의미이다. Unbounded WildCard라고 알려져 있다.

(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());         // 불가능(컴파일 에러)
}

f) UserService

@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();
    }


}
  • isPresent(), ifPresent(): Optional 객체가 값을 가지고 있는지 확인할 수 있는 메소드로, Optional<>을 사용했을 때 발생할 수 있는 NullPointerException을 회피하기 위해 사용할 수 있다.
    - isPresent(): Boolean 타입을 return한다 - 값이 없으면 false, 있으면 true
    
        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()));});

C. 테스트 코드 작성

@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로 예외가 발생했는지 확인 

    }
}

  1. Enum에 대한 설명 (1) : https://www.nextree.co.kr/p11686/
  2. Enum에 대한 설명 (2) : https://techblog.woowahan.com/2527/
  3. 에 대한 설명 : https://pathas.tistory.com/160
  4. 와일드 카드: https://mangkyu.tistory.com/241

0개의 댓글