1차 과제와 비슷하게 Kakao API를 활용해서 HTTP 요청을 보내고, 응답을 받는 주제로 과제를 진행했다.
동작 - 데이터 - 객체 - 클래스 순서로 객체를 모델링 해야하는데 아직 참 어렵다. 그래도 더 나아질 부분만 남았다는 뜻이니깐 객체지향 공부를 더 해야겠다.
문제 1
JDBC를 사용하기 위해 Driver에 연결된 Connection을 인자로 받고, List<BookDto> list를 DB 저장소로 저장하는 메서드 이다. PreparedStatement 객체를 생성해서 사용을 하는데 Price와 Sale_price를 넣는 과정에서 SQL 문 ? 자리에 매칭이 되지 않는 문제가 있었다. intValue를 붙여줘서 잘 동작하긴 하지만 IDE에서 필요 없는 변환이라고 도움말이 뜨긴 한다. 이 부분에 대해서는 PreparedStatement 에 대해 더 알아봐야 할 것 같다.
느낀 점
1차 과제는 빈 프로젝트에서 아무런 설계 없이, 클래스를 하나 하나 만들어 가면서 과제를 진행했다. 점점 진행 할 수록 과제가 산으로 가고 있다는 느낌을 받았다. 지난 1차 과제와는 다르게 공책에 연필로 직접 관계도를 그려가면서 클래스를 설계했다. DB와 관계를 가지는 JdbcRepository.class를 구상하고, Kaka API와 통신하는 HttpClient.class를 중심으로 과제를 진행했다. 멘토님께서 말씀해주신 방식은 동작 - 데이터 - 객체 - 클래스 순서로 객체를 모델링 이 순서이다. 이 문장을 봤을 때, 아직 어떻게 시작을 해야할 지 막막하다는 생각이 들었다. 멘토님께서 추천해주신 책을 주문을 하고 그 책을 읽어보면서 객체지향에 관한 개념을 다시 한 번 정리해야할 것 같은 필요성을 느꼈다.
과제를 처음 진행할 때 출력 반복문의 인덱스를 정하기 위해서 객체의 수를 알아내기 위해 공식 API를 참고했다. 응답내용에 meta -> total_count가 응답받은 결과의 갯수인 줄 알고 그 값을 기준으로 출력할 수 있도록 반복문을 작성했다. 결과는 아래의 표와 같다.
반경(km)
total_count
실제 응답 받은 객체 수
0.3
2
2
0.5
20
15
공식 문서에는 검색 된 문서의 총 수를 응답받지만 결과가 달라서 내 코드를 신뢰할 수 없었다. HTTP 요청 테스트 방법이 익숙치 않아서 시간을 아끼고자 다른 방법을 사용했다. (추후에 HTTP 요청 테스트 하는법 공부하기) 원래는 오브젝트 정보를 받아오자마자 날 것으로 프린트하려고 했지만 List에 담아서 size()를 활용했다.
아래의 그림은 API로 검색할 땐 결과가 나오지 않지만, 카카오 맵에서는 결과가 표시된다는 질문에 대한 답변이다.
total_count의 "검색 된 문서의 수" 가 API 내에서의 검색 수인지, 카카오 맵에서의 검색 수인지 잘 모르겠지만 값이 다른 이유라고 의심해볼까...?
문제 2
과제의 실행 결과 화면에서는 "00시 ㅁㅁ아파트" 와 같이 아파트 이름의 주소를 입력해 값을 응답받았는데 같은 주소를 내 코드에 실행시켜보니 실패했다. 이유를 찾다보니 정확하지 않지만 카카오 API 공지사항에서 아래와 같은 글을 찾았다.
과제의 예시처럼 ㅁㅁ 아파트로는 검색이 안되었지만, 해당 아파트의 도로명 주소? 00도 00시 00동 으로 검색하니 잘 동작하였다. 위의 문제인지는 잘 모르겟지만 일단 마무리 지었다.
++ 그룹 스터디 조원분들이 알려주셨다. 나는 "주소 검색하기"로 x, y 좌표를 구했는데 "키워드 검색하기" 로 검색을 했어야 했다. 다음부턴 문제를 잘 읽자 ㅎㅎ
느낀점
Codecomplexity 를 쓰니 코드의 복잡성에 대해 다시 생각하게 된다. 위의 코드는 list를 출력하는 역할을 하는 메서드이다. 두 개의 메서드 모두 같은 기능을 하지만 어떻게 구현하느냐에 따라 코드의 CodeComplexity를 점수로 나타내주는 플러그인을 통해 메서드를 분리한다던지, 의존성을 줄인다던지 등 완벽하진 않지만 더 나은 코드를 작성할 수 있는지에 대한 고민을 할 수 있는 기회를 자연스럽게 갖게 되었다.
Spring boot를 처음 공부를 할때는 어노테이션을 기반으로 한 생성자 주입, 예외처리, 벨리데이션을 배웠는데
순수 자바 코드로 구현을 하려니깐 막막했다. 예외를 잡기위해 try catch를 남발하고 의존성 주입을 lombok 어노테이션으로만 구현을 했었는데 필드주입으로 구현한 점 등,
멘토님께서 DTO와 Interface를 언급해주셨다. 주먹구구식으로 개발한 내 과제에서 코드를 수정하긴 했지만 DTO가 어떤 역할을 하는지, 왜 필요한지 자세하게 설명할 수 없었고 깡통 인터페이스를 implements했지만 의미가 있는지 등 의미 없이 코드를 가져다 붙인 것 같다. 다음 과제부터는 주먹구구식으로 시작하지 말고 객체 설계, 연관 관계를 잘 고려해 개발을 똑똑하게 시작해야겠다.
따라서, ResponseBody와 같이 리턴하는 응답 값이 body로 설정되어 클라이언트에게 전달된다.
아래와 같이 IndexOutOfBoundsException을 발생하도록 설정하고 HTTP Get요청을 보내면 아래 그림과 같이
Http Status Code 500번과 Internal Server Error이 반환된다.
@Slf4j
@RestController
@RequestMapping("/api")
public class RestApiBController {
@GetMapping(path = "")
public void hello() {
var list = List.of("hello");
var element = list.get(1);
log.info("element : {}", element);
}
}
예외가 발생해도 자연스럽게 응답을 내려주기 위해서는 서버에서 컨트롤러에 대한 예외를 처리해야 한다.
try catch로 직접 처리할 수 있지만 코드 길이가 길어질 수 있으므로 @RestControllerAdvice 를 사용한 Handler 클래스를 만들어서 사용한다.
@RestControllerAdvice 가 붙은 클래스는 Rest API Controller에서 예외가 일어나는 것을 감지
@ExceptionHandler로 어떠한 예외를 캐치할 것인지 선언 가능, 아래의 코드에선 (IndexOutOfBoundsException)을 value 값으로 설정했으므로 해당 예외를 처리한다.
@Slf4j
@RestControllerAdvice
//-> 아래 클래스는 rest api를 사용하는 곳에 에러를 감지함
public class RestApiExceptionHandler {
@ExceptionHandler(value = { IndexOutOfBoundsException.class })
public ResponseEntity outOfBound(
IndexOutOfBoundsException e
){
log.error("IndexOutOfBoundsException",e);
return ResponseEntity.status(200).build();
}
}
ResponseEntity를 반환하기에 HttpStatus 코드를 200으로 변경하여 반환하면 이전과 다르게 예외가 핸들링된 것을 볼 수 있다. @ExceptionHandler(value = { IndexOutOfBoundsException.class }) 의 value 값을 Exception.class로 바꾸게 되면 전체적인 Exception 하위의 예외들을 한 번에 처리할 수 있다.
다른 방법으로 따로 ExceptionHandler을 관리하지 않고, 컨트롤러 단에서 직접 @ExceptionHandler을 사용하여 해결할 수 있다. 아래 코드는 위와 같은 예제에서 Controller 내부에서 @ExceptionHandler 를 적용한 코드이다. 코드 길이가 길어질 수 있으므로 이 방법은 특별한 컨트롤러에서만 사용을 한다.
@Slf4j
@RestController
@RequestMapping("/api/b")
public class RestApiCController {
@GetMapping("")
public void hello() {
throw new NumberFormatException("number format exception");
}
@ExceptionHandler(value = NumberFormatException.class) //->글로벌로 가지 않고 여기서 해결
public ResponseEntity numberFormatException(
NumberFormatException e
){
log.error("RestApiCController", e);
return ResponseEntity.ok().build();
}
}
value 값에 클래스를 명시하는 것 이외에도 basePackages를 명시할 수 있다. @RestControllerAdvice 의 value값에 패키지 경로를 명시하게 되면 해당 패키지에 속한 컨트롤러에서 일어나는 모든 예외를 하나의 핸들러로 관리할 수 있다.
@Slf4j
@RestControllerAdvice(basePackages = "com.example.exception.controller") // 패키지 명시
@Order(1)
public class RestApiExceptionHandler {
@ExceptionHandler(value = { IndexOutOfBoundsException.class })
public ResponseEntity outOfBound(
IndexOutOfBoundsException e
){
log.error("IndexOutOfBoundsException",e);
return ResponseEntity.status(200).build();
}
@ExceptionHandler(value = NoSuchElementException.class)
public ResponseEntity<Api> noSuchElement(
NoSuchElementException e
){
log.error("",e);
var reponse = Api.builder()
.resultCode(String.valueOf(HttpStatus.NOT_FOUND))
.resultMessage(HttpStatus.NOT_FOUND.getReasonPhrase())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(reponse);
}
}
ExceptionHandler가 2개 이상인 경우 @Order을 통해 먼저 처리될 수 있는, 우선순위를 부여할 수 있다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@Slf4j
@RestControllerAdvice(basePackages = "com.example.exception.controller") // 패키지 명시
@Order(1)
public class RestApiExceptionHandler {
@Order <<<< spec >>>>
int value() default Ordered.LOWEST_PRECEDENCE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
@Order의 우선순위를 설정할 때 기본 값은 LOWEST_PRECEDENCE = MAX_VALUE 로 설정되어 있다.
즉 @Order가 있을 경우, value가 낮은 친구 먼저 처리된다.
Order vs Order(1) : default가 MaxInt이므로 그보다 낮은 1이 실행
Order(2) vs Order(3) : 2가 실행
Order vs Order : Class 이름 순으로 실행
먼저 처리되고 싶으면 value에 낮은 값을, 나중에 처리되고 싶으면 높은 값을 주면 된다.
위의 GlobalExceptionHandler은 RestApiExceptionHandler에서 (처리하지 못한, 예상치 못한) 나머지 예외를 받아내기 위한 최후의 역할을 하므로RestApiExceptionHandler 의 Order(value)값 보다 높게 값을 설정하여 RestApiExceptionHandler가 먼저 처리되게 한다.
(위의 예시 상황은 Oder가 없는 경우 vs 있는 경우임... 만약 Order가 둘 다 있을 경우엔 값이 낮은 친구부터)
메소드가 반환할 결과 값이 '없음'을 명백하게표현할 필요가 있고,null을 반환하면 에러가 발생할 가능성이 높은 상황에서 메소드의 반환 타입으로Optional을 사용하자는 것이Optional을 만든 주된 목적이다. Optional타입의 변수의 값은 절대null이어서는 안 되며, 항상Optional인스턴스를 가리켜야 한다.
Optional<T> 생성, 선언
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //Null값 까지 받을 수 있음-> Null이면 Empty 객체 생성
//Optional.of(T) -> 명시한 T값을 가지고 있는 객체 생성, Null일 경우 Error
//Optional.empty() ->비어있는 객체 생성
}
isPresent(True or False), ifPresent(Consumer<? super T> action)
값이 존재하면 True or False, 값이 존재하면 action을 취하라
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m ->{
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
접근제어자가 protected로 설정되었다면, 동일 패키지의 클래스 또는 해당 클래스를 상속받은 다른 패키지의 클래스에서만 접근이 가능하다.
public vs protected
public은 외부에서 접근할 수 있지만 protected는 외부에서 접근할 수 없다.
어떻게보면 public와 protected는 비슷하다고 볼 수 있다.
protected는 원하는 클래스의 메소드를 직접 instance를 만들어 사용할 순 없지만, 상속 관계가 되는 경우
자유롭게 이용이 가능하기 때문이다.
그럼 오직 외부에서의 접근을 막기 위해 protected를 써야 하는지에 대한 궁금증이 생겼기에
구글링을 통해 OOP에서의 protected의 역할을 찾아봤다.
protected는 잠재적으로 자식 클래스가 오버라이드(Override)해서 바꾸어야 할 경우를 고려한 접근제어자이다.
아래의 새(Bird) 코드와 타조(Ostrich)클래스 예시를 보자
public class Bird {
void fly() {
System.out.println("I am flying");
}
protected void moveFast() {
fly();
}
}
public class Ostrich extends Bird{
public static void main(String[] args) {
Ostrich ostrich = new Ostrich();
ostrich.moveFast();
}
void run(){
System.out.println("I am running");
}
protected void moveFast(){
run();
}
}
타조는 Bird 클래스를 통해 moveFast() 메서드를 상속받았지만 다른 새들과 달리 fly()를 사용할 수 없기에
불가피하게 moveFast()를 오버라이드를 통해 변경해야 한다. 따라서 변경이 짐작되는 메서드는 protected를 통해 변경의 요지를 표시하는 용도로 사용될 수 있다.
Object, Clonabel인터페이스, clone()메서드
위 사진은 Object에서 사용할 수 있는 메서드 종류이다. clone, finalize 메서드는 protected 접근제어자를 가진다.
finalize메서드는 Deprecated
Clonabel 인터페이스(믹스인 용도) : 일반적인 인터페이스랑 다르다. 아무 내용이 없다, clone()메서드의 동작방식을 결정한다. (믹스인 : 객체지향에서 다른 클래스에서 사용할 목적으로 만들어진 클래스) Clonable 인터페이스를 구현하지 않은 상태에서 clone()를 호출하면 CloneNotSupportedException을 던진다.
기본적인 사용법은 clone()를 사용하려는 클래스에서 Clonable 인터페이스를 구현하고, clone()를 오버라이딩 해서 사용한다.