뭘 배웠을까? - 우아한테크코스 Lv3

6 분 소요

정리하고 보니, 웹 전반적으로 개념적인 이해가 된 것 같긴 하나 정리를 하기는 어렵다. 웹 서버의 전체적 흐름과 우리가 실제 사용하는 Spring framework의 흐름과, 그 구현을 왜 그렇게 했는지에 대한 이해를 조금은 할 수 있는 기회였다. 하지만 정리를 하면서 보니 lv1, lv2에 비해 정리해둘 내용이 없는 것 같아 아쉽다. 개념적으로 배우고 어떤 흐름인지 알게 되었지만, 이를 글로 정리하는 능력이 부족하여 정리하지 못했다.

학습 목표

  • 웹 서버를 직접 만들어 보는 경험을 통해 HTTP에 대한 이해도를 높인다.
  • TCP/IP와 같이 다양한 프로토콜을 분석해 네트워크의 기본 역량을 쌓는다.
  • 나만의 라이브러리를 직접 구현해 보는 경험을 통해 업무에서 발생하는 중복 코드를 제거하는 역량을 쌓는다.
  • 대용량 데이터에 처리에 대한 역량을 쌓는다.
  • MVC, DI 컨테이너를 직접 구현해 보는 경험을 통해 Spring 프레임워크의 내부 동작 원리에 대한 이해도를 높인다.
  • 성능을 고려해 시스템을 설계하고, 구축하는 경험을 한다.
  • WAS 구현
    • WAS 구현을 통해 HTTP 이해
    • HTTP를 이해해 성능 좋은 웹 애플리케이션 개발
    • HTTP 세션 구현을 통해 세션 이해
  • MVC 구현
    • MVC 프레임워크 구현을 통해 자바 reflection과 MVC 패턴 이해
    • 점진적으로 MVC 프레임워크를 전환하는 전략
    • MVC에 Restful API 처리를 위한 기능 추가
  • JDBC 구현
    • 나만의 JDBC 라이브러리 구현을 통해 중복 제거(5주)
    • JVM Stack vs Heap
    • 대용량 데이터 조회 쿼리 연습, index를 통한 성능 개선 경험(6주)
  • DI 구현
    • DI 프레임워크 구현을 통해 DI 개념과 Spring 프레임워크 이해
    • AOP 개념 및 Spring AOP 적용
    • Transaction과 Spring Transaction

뭘 배웠을까, 받았던 피드백

  • uri가 단순히 extension을 contain하고 있는지만 봐도 될까요~? query string을 제외하고 end with extension한지 확인하면 어떨까요~?
    • 아래 코드처럼 구현했었다. 그래서 queryString에 만약 html, css 이런 것들이 들어있거나, 파일 이름에 html, css와 같은 것이 들어가면 잘못된 동작을 할 수 있다.
public static String getFullPath(String uri) {
    return Arrays.stream(values())
            .filter(value -> uri.contains(value.extension))
  • 생성자에서 uri를 받아 httpStatusLine, httpResponseHeader, httpResponseBody를 설정하는 건 어떨까요~?
  • getToIndex 메서드명을 조금 변경해서 lines.subList(1, getToIndex(lines))의 연산까지 진행하면 어떨까요~?
  • requestLine이나 uri클래스를 만들어 물어보는건 어떨까요~?
  • RequestParser가 Request에게 List 이 아니라 RequestLine, RequestHeader, RequestBody를 만들어 전달하면 어떨까요~? 생각을 여쭤보는거니 꼭 변경해야 한다고 생각하지 않으셔도 됩니다:)
  • responseHeader는 웹서버가 만들어 반환하는데 String 말고 다른 정형화된 값을 받아도 되지 않을까 생각이 듭니다. 혹시 어떻게 생각하시나요~?
  • 메서드를 기능에 따라 분리하면 어떨까요~?
  • uri에 호스트정보까지 들어오지 않습니다:) 현재 uri에 대한 validation이 구현되어 있지 않기 때문에 상관없지만 잘못된 성공 예일수 있으므로 변경하는 것이 좋습니다:)
    • 잘못된 테스트는 테스트가 없는 것 보다 안 좋은 결과가 나올 수 있다.
  • route가 httpResponse를 반환하고 그 값을 ResponseWriter가 write하면 어떨까요~?
  • ” “와 같은 체크도 처리하기 위해 StringUtils를 사용해 보면 어떨까요?
    • StringUtils.isBlank사용
  • 세션의 키를 외부에서 주입받는데 도메인상 맞지 않습니다. 특히나, 세션의 키는 UUID 기반으로 생성되어야 한다는 요구사항이 있는데요. SessionKeyGenerator가 필요해 보입니다. (public void setAttribute(String key, Object value) {와 같은 식으로 구현했었음)
    • 레벨 2에서 세션 사용할 때 Session.setAttribute(“user”, loginId) 처럼 사용했던 기억이 있어 이와 같이 구현했습니다. 그리고 session의 id는 SessionManager 클래스가 Session을 만들 때 UUID 기반으로 생성하고 있는데, 이 로직을 따로 SessionKeyGenerator로 뺀다면, Generator에서 UUID 방식과, 고정 값을 받아서 Session의 key를 생성하는 방식으로 변경하면, 랜덤을 제외한 방법은 테스트를 할 수 있다는 장점이 있어서, SessionManager에서 SessionKeyGenerator의 구현체를 받아서 생성하도록 변경함
    • 스프링 썼을 때 session.setAttribute(“user”, loginId)와 같은 방식. session.getAttribute(“user”) 방법으로 Object를 꺼낼 수 있음
  • 세션의 경우 멀티 쓰레드 환경에서 여러 쓰레드가 동시에 접근하는 객체입니다. 일단은, ConcurrentHashMap으로 처리해 보는게 어떨까요?
  • session 테스트시 키값에 대한 테스트는 항상 랜덤한 테스트밖에 지원이 안될것 같네요. 개선해 보면 어떨까요?
    • SessionKeyGenerator의 구현체를 받아서 테스트를 할 수 있도록 변경(전략패턴 사용 - FixedKeyGenerator와 같은 느낌)
  • IllegalArgumentException 의 예외 의미가 현재 동작과 맞는 의미일까요?
    • 커스텀 예외 만드는 것을 주저하지 말자.
  • Hanlder 와 View 가 확장되면 분기문이 늘어가는 형태가 될 것 같아 확장에 좋은 추상화가 아직 아니라고 생각해요. Handler 와 View 가 확장되어도 분기문이 늘지 않을 수 있도록 Handler 와 View 에 대한 처리를 각각 추상화해보는 것은 어떨까요?
    • HandlerAdpater와 ViewResolver 등으로 추상화 할 수 있다.
  • 매번 인스턴스가 생성될 필요가 있을까요?
  • 확인이 가능한 테스트는 assert 로 작성하셨으면 좋겠네요 :)
    • logger나 print로 테스트하는 것이 아닌 assertEquals, assertThat, assertThrows 등의 assert로 확인 한다.
  • ServletException 과 Logger 가 모두 stack 정보를 삼켜버려 추적하기 쉽지 않겠네요 :)
    • logger에서 logger.debug("Exception: {}", e.getMessage())로 구현 했었는데, 이럴 경우 stack trace를 모두 먹고 message만 출력해버린다. 디버깅 하기 굉장히 어려워진다.
  • Map에 담긴 model 데이터가 1개인 경우 value 값을 반환, 2개 이상인 경우 Map 자체를 JSON으로 변환해 반환한다. 요구사항이 만족된 것이고, 정상적인 JSON 이 생성되는 것이 맞는지 확인해주세요.
  • 중복 행위를 없앨 수 있지 않을까요?
  • ToBeHandlerAdapter 네이밍을 의미있게 했으면 좋겠네요.
  • Controller 가 수행하기에 어울리지 않아 보이네요 :)
  • ExceptionUtils.getStackTrace를 사용하는 이유는 무엇인가요?
    • 로거의 구현체에서 이미 구현되어있다. logger.info("Exception: {}", e);와 같은 식으로 throwable을 파라미터로 넘길 수 있다. 그러면 stack trace를 쭉 볼 수 있다.
  • debug 레벨은 로컬 개발에서 확인을 위한 레벨입니다.
  • 실패했다는 정보가 의미없게 버려지는게 맞는 것일까요? 실패하여도 응답은 같은 결과를 반환하나요?
    • 디버깅 레벨을 조정했다.
  • else 의 사용은 코드 가독을 어렵게 만듭니다. 코드를 따라 읽다가 다시 조건을 확인해야하니 말이죠. 이럴 때 early exit 또는 return early 라는 패턴을 적용할 수 있습니다. 예외가 되는 케이스를 먼저 처리하고 반환하여 코드 가독의 흐름을 쉽게 도와줄 수 있을 것 같습니다. (중복도 자연스럽게 제거가 되겠네요)
  • 일반적으로 개발 환경을 로컬-개발-운영 으로 나눌 때 개발,운영 환경에서 로거레벨은 info 를 사용합니다. 로컬을 제외한 환경에서 debug 레벨은 버려지게 됩니다.
  • 타입을 보장하지 못해서 런타임시 에러가 발생할 것 같은데 Class<T> 로 변경해보는 것은 어떨까요?
    • clazz는 wildcard고 return type은 T라서 타입이 보장되지 않는다. 만약 아래 코드에서 UserCreatedDto.class 대신 이상한 클래스가 들어가더라도 컴파일에는 문제가 없지만 런타임에 문제가 발생한다.
  • JDBC Template 구현
    • reflection을 사용해서 내부에서 ResultSet으로 객체를 만들어 주는 것 public <T> List<T> query(String query, Class<?> clazz, Object... objects)
      • 객체의 클래스 타입만 넘겨주면, JDBC template 내부에서 알아서 객체 매핑을 해주는 것이 사용자 입장에서는 더 편할 것 같아서 구현해본 방법
    • UserDao에서 RowMapper의 mapRow()를 구현방법을 받아서 객체 생성 하는 것 public <T> List<T> query(String query, RowMapper rowMapper, Object... objects)
      • UserDao에서 객체 생성 방법을 직접 받아서 객체를 생성
// public <T> T queryForObject(String query, Class<?> clazz, Object... objects)
User user = jdbcTemplate.queryForObject(
                "SELECT userId, password, name, email FROM USERS WHERE userid=?",
                UserCreatedDto.class,
                userId);
  • 예외 케이스까지 테스트해보면 더 좋을 것 같아요!
    • 실패하는 테스트, 예외 상황에 대한 테스트가 항상 필요하다.
  • BeanScanner가 한번만 사용되는데 필드에 저장할 필요는 없지 않을까요?ㅎㅎ
  • 보통은 인젝션된 빈에 대해 getter를 따로 생성하지 않는데 테스트를 위해 만드신것 같네요. 이보다는 리플렉션을 사용하여 테스트하는게 어떨지 의견드립니다. 필드값을 받아서 해당 필드에 대한 타입과 인스턴스가 일치하는지 확인만 한다면 테스트가 가능할 것 같아서요ㅎㅎ

Logger

로깅 레벨을 적절히 정하는 것이 매우 중요하다. debug는 로컬 개발에서 확인을 위한 레벨이다. 일반적으로 로컬을 제외한 환경에서는 debug 레벨은 버려지게 된다. 그래서 실제로 코드를 짜는데 로깅레벨을 설정하는 것은 매우 중요하다. exception을 로깅하는 경우에 e.getMessage로 exception의 다른 정보들을 다 날려버려도 상관 없는지 항상 생각해보자, 습관적으로 e.getMessage()를 쓰는 경우가 많이 있다.

아래는 kingbbode님의 피드백.

logging은 굉장히 중요합니다 :)

테스트가 아무리 견고하게 작성되어도 항상 예상치못하는 이슈들이 생깁니다. 이럴 때 로그가 없다면 결국 확인될 수 없이 뇌피셜로 이슈를 마무리하게 될 수 밖에 없습니다.

그렇다고 모든 로그를 남기게 될 때 의미있는 로그들을 수집하는데 어려움을 겪을 뿐만아니라, 성능상으로 문제를 겪을 수도 있습니다. 비동기 로거를 사용한다고 하더라도, 어느정도 성능 이슈가 생길 가능성이 있고, 비동기 로거의 방어로직으로 의미있는 로그가 유실될 가능성도 있습니다.

그래서 로그의 레벨을 잘 다루는 것이 중요합니다. 우리가 무엇을 의미있는 데이터로 볼 것인지를 명확히 할 수 있고, 의미있게 작성된 로그로 인해 비교적 편하게 이슈의 원인을 명확히 파악할 도수 있습니다. 사소한 것이지만, 개인적으로 중요한 부분이라 생각하여 머지하지 않고 피드백을 드렸었습니다 :)

고생하셨습니다 👍

트랜잭션 관리

try {
    con.setAutoCommit(false);
    con.commit();
} catch (SQLException e) {
    con.rollback();
    throw e;
}

이런 식으로 구현할 필요 없다. connection 한번 당 어차피 쿼리는 하나가 날아가는 것이고, 해당 쿼리가 실패하면 롤백을 할 필요가 있는게 아니라, 쿼리가 실패했기 때문에 DB가 업데이트, 삭제등 쿼리의 영향을 받지 않는 것이다.

트랜잭션은 하나로 생각해야 하는 작업 단위라고 생각하면 될 것 같다. A resource를 수정하고, B resource를 수정하는 것이 하나의 트랜잭션이라면, update A 이후에 쿼리가 실패한다면 이 전체 트랜잭션을 롤백해야 하기 때문에 위와 같은 코드가 의미가 있지만, 우리가 구현했던 코드에서는 단일 쿼리이기 때문에 이에 대한 롤백 같은 작업이 필요가 없다.

DI 미션 구현

BeanFactory내부에서 this.beanScanner = new BeanScanner(basePackages);와 같은 방식으로 Scanner 생성을 하고 있습니다. jwp-mvc 모듈은 jwp-di 모듈의 사용자 입장이라고 생각해서 BeanScan 동작까지 알아야하나? 라는 생각으로 BeanFactory 내부에서 Scanner에 대한 책임을 갖도록 했다.

ApplicationContext에서 basePackage기준으로 생성자 방식, 메서드 방식(Config의 Bean 어노테이션)으로 각각 빈 생성 방식(BeanCreateMatcher Class)에 맞게 빈을 찾습니다. (AnnotationBeanScanner, ConfigurationBeanScanner)

찾은 BeanCreateMatcher를 가지고 BeanFactory에서는 각 빈의 생성 방식인 BeanCreateMethod와 InstantiationMethod를 사용해 빈의 오브젝트를 만듭니다.

ApplicationContext의 경우는 기존처럼 basePackages 경로를 넣어줄 수도 있고, Configuration Class를 넣어줄 경우 어노테이션의 basePackages를 기준으로 스캔하도록 했습니다.

태그:

카테고리:

업데이트:

댓글남기기