29cm & 아키드로우 과제 회고록

tony·2024년 4월 6일
31

회고

목록 보기
11/11

어떤 관점에서 접근했는가?


각각의 요구하는 부분과 평가사항이 달랐다.

하지만 유출할 수 없으니 여기에 기입할 수는 없고, 요컨대 다음과 같았다.

29cm 에서는 OOP 에 대한 깊은 이해도를 요구했고,

아키드로우에서는 설계능력을 비롯한 실제 개발 실력을 파악하고자 했다.

결국 각각 기업에서 요구하는 관점에서 어떻게 기능구현을 충실히 하는지가 중요했다.

그 뿐이었다 ㅎㅎ

기업 블로그가 있다면 관련 자료를 철저히 살펴보는 게 중요하다고 느꼈다.

왜냐하면 29cm 에서는 개발문화가 OOP 를 중요하게 생각하는만큼,

몇몇 포스트에서 OOP 에 대한 향기를 진득하게 풍겼다.

특히 과제를 구현하며 아래 포스트에서 영감을 많이 받았다

값 객체(Value Object)를 활용하여 변경 용이성 개선하기

또한 각각에 맞춰서 README 를 철저히 쓰는 것도 중요하다고 느꼈다.

README 를 읽고 “이 친구는 어떤 생각을 가지고 접근했구나”를 파악하기 때문이다.

소설을 읽게 하듯이, README 라는 목차를 통해 서사를 확실하게 하면

코드라는 소설의 몰입도와 재미는 배가 되기 때문이다.

체크리스트 만들고,

꾸준히 기능개발하고,

README 수정하고,

다시 뜯어내서 리팩토링하고,

무한반복이다.

이후로는 충실히 기능개발에 임하는 수밖에 없다 ^^

무엇을 사용하였고 왜 사용하였는가?


  1. draw.io

    이만한 게 없다.

    책임 및 역할 분리, 엔티티 설계, ERD, 아키텍처 설계, 디자인패턴이나 레이어에 대한 고민 등등

    이 모든 게 하나의 툴로 처리가 가능하다.

    평소에 유용하게 쓰고 있어서 그대로 사용했다.

  2. 체크리스트.md와 TDD

    기능구현에 앞서서 어떤 구조로 접근할지 draw.io 로 처리를 하였다면, 이제 문서화를 할 차례다.

    체크리스트.md 를 통해 어떤 기능을 어떻게 만들지 나열을 하였다.

    특히 예외케이스, 테스트케이스들을 추출하여 어떤 기능을 점검해야할지 - [ ] 을 통해 점검표를 만들었다.

    하나씩 만들어갈 때마다 체크표시하고, 생각이 바뀌어 구조나 구현과정이 변경되어야한다면 체크리스트도 수정해줬다.

    평소에도 꾸준히 이 과정을 적용했지만, 가장 큰 핵심은 "살아숨쉬는 문서화를 지향하자" 였다.

    그래야만 계속해서 구현과 동시에 요구사항에 대한 진척도를 확인할 수가 있다.

    이후에는 TDD 를 적용하여 구현하기 시작했다.

    컨트롤러나 서비스의 뼈대를 먼저 세우고, 바로 test 를 짰다.

    test 에 아무런 값이 없어도 된다. test 를 짜면서 동시에 구현을 하면된다.

    항상 구현 이후에 테스트케이스를 짜곤 했는데, 이게 사실 더 시간이 오래 걸린다는 걸 이번에 알았다.

    어차피 테스트케이스 짤 때 구현물을 다시 되돌아봐야하기 때문이다.

    테스트와 구현을 동시에 하면 그 간극이 줄어 금방 만들 수 있다.

    더군다나 TDD 의 장점인 기능동작을 중점으로 개발을 할 수 있다.

    물론,,, 모든 TDD 반대론자들이 말하는 “빨리 배포되어야 하는 기능이라면 TDD 적용은 쉽지 않다” 에 동의하는 건 예나 지금이나 같다 ^^

  3. 추상화 적극 활용

    일단 바뀔 수 있는 부분이라면 무조건 추상화를 하였다.

    과제 내용은 말할 수 없지만, 가령 View 에 대해서는 추후 HTML 이 아닌 EXCEL 출력, 심지어 콘솔출력으로도 바뀔 수 있는 부분이다.

    이렇게 나중에 고객이 바꿔달라고 하면 바꿔야 하는 부분이라면 무조건 추상화를 해주었다.

    *아키드로우 과제에서는,,,, 이 부분을 까먹고 다 구현체로 구현하였다,,,, 하하,,,,^^

  4. MapStruct

    변환과정과 코드에 대해

    • 직접 짜지 않아도 되고
    • 책임분리까지 할 수 있다

    왜 안 쓰는가? 이유가 없다면 당장 쓰자.

  5. DTO 에 관한 record

    자세한 건 생략한다.

  6. VO 와 일급컬렉션을 적극활용했다.

    • VO 에 대한 중요성은 29cm 포스트를 많이 참고하였다. 값 객체(Value Object)를 활용하여 변경 용이성 개선하기
    • 일급컬렉션을 활용하여 최대한 도메인 개념을 집어넣었다.
      • 가령 “장바구니” 라는 개념을 구현해야했다.
      • 일급컬렉션을 활용하면 “장바구니라는 도메인 개념을 부여” 할 수 있다.
      • 이에 따라 장바구니에 대한 동작을 외부에서 제어하는 게 아닌 장바구니 자체가 본인의 책임과 역할을 이행할 수 있게 할 수 있다.
  7. Custom Annotation

    두 과제에서 모두 요긴하게 활용되었다.

    두 가지 개념을 구현하기 위해 적용했다.

    • Custom Validation
    • Entity 에 대한 data 처리

    Custom Validation 은 뭐,, 다들 써봤다 싶이 Validator 를 따로 구현해주는 것이다.

    Entity 에 대한 data 처리에 대해서는 컬럼에 대한 한글명과 자간수를 엔티티에서 보관하고, 이에 대해 View 가 알아서 처리할 수 있도록 할 수 있을까 라는 관점에서 처리했다.

    아래와 같이 활용하였다.

    Entity

    @Column
    @KoreanColumnName("상품명")
    @KoreanColumnFormat("%-60s")
    private String productName;
    
    @Column
    @KoreanColumnName("판매가격")
    @KoreanColumnFormat("%-10s")
    private Integer sellingPrice;
    
    @Column
    @KoreanColumnName("재고수")
    @KoreanColumnFormat("%-10s")
    private Integer stockQuantity;

    View

    private String generateHeader() {
        StringBuilder headerBuilder = new StringBuilder();
        for (Field field : fields) {
            String columnName = getKoreanColumnName(field);
            String columnFormat = getKoreanColumnFormat(field);
            if (columnName != null && columnFormat != null) {
                headerBuilder.append(String.format(columnFormat, columnName));
            }
        }
        return headerBuilder.toString();
    }
    
    private void printProductInfo(Product product) {
        StringBuilder productBuilder = new StringBuilder();
        for (Field field : fields) {
            generateProductInfo(product, field, productBuilder);
        }
        System.out.println(productBuilder);
    }
    
    private void generateProductInfo(Product product, Field field, StringBuilder productBuilder) {
        field.setAccessible(true);
        String columnName = getKoreanColumnName(field);
        String columnFormat = getKoreanColumnFormat(field);
        if (columnName != null && columnFormat != null) {
            try {
                Object value = field.get(product);
                productBuilder.append(String.format(columnFormat, value));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    
    private String getKoreanColumnName(Field field) {
        KoreanColumnName annotation = field.getAnnotation(KoreanColumnName.class);
        return annotation != null ? annotation.value() : null;
    }
    
    private String getKoreanColumnFormat(Field field) {
        KoreanColumnFormat annotation = field.getAnnotation(KoreanColumnFormat.class);
        return annotation != null ? annotation.value() : null;
    }
  8. http client

    허구한 날 swagger, postman 쓰는 것 보다 이게 진짜 찐이다 라고 느꼈다.

    개발하면서 바로 옆 tab 을 통해 — 심지어 단축키도 ctrl+shift+f10 이다 — 바로바로 확인이 가능하다.

    절대 다시는 안 버리겠다.

  9. h2, data.sql, schema.sql

    아 정말 애를 많이 먹은 친구이다.

    datagrip 을 평소에 사용하다보니, 보통 schema 를 따올 때는 export file 을 통해 가져오기 마련이었다.

    하지만, 이렇게 가져온 create 문은 h2 와 들어맞지가 않는다는 사실,,,,

    몇 시간을 헤맸는데 gpt 를 통해 h2 syntax 로 변환해주니 잘 돌아가는 게,,,, 정말 믿고 싶지 않았다,,,

  10. WebClient 와 HTTP Interface

    과제 중에 OpenAPI 를 처리해야하는 과제가 있었는데 결합도 측면과 에러처리를 해주기 위해 WebClient 와 HTTP Interface 를 사용하였다.

    WebClientConfig 를 통해 WebClient 에 대한 빈을 처리해주었다.

    Config 구현 시, HttpStatusCode 응답이 에러인 경우 커스텀예외를 뱉어내게끔 처리해주었다.

    아래와 같이 활용했다.

    @Value("${webclient.url}")
    private String baseUrl;
    
    @Bean
    CylinderWebClient getCylinderApi() {
        WebClient webClient = WebClient.builder()
            .baseUrl(baseUrl)
            // HttpStatusCode 가 예외인 경우, 커스텀 예외인 WebClientCustomException 을 뱉음
            .defaultStatusHandler(HttpStatusCode::isError, resp -> {
                log.error(resp.toString());
                return resp.bodyToMono(ErrorDetail.class)
                    .flatMap(errorDetail ->
                        Mono.error(new WebClientCustomException(resp.statusCode(),
                            new ErrorDetail(null, null, resp.createError().toString(), LocalDateTime.now()))));
            })
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
        return HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient))
            .build()
            .createClient(CylinderWebClient.class);
    
  11. DB 연결 오류 및 트랜잭션 예외발생에 대한 예외처리 핸들러를 구현하였다.

    최대한 다양한 예외케이스에 대해 신경쓰고자 했다.

    @RestControllerAdvice를 통해 아래 케이스드를 처리했다.

    • I/O예외 발생 시 예외처리 핸들러
    • 형식에 어긋난 입력형식오류예외 발생 시 예외처리 핸들러
    • 커스텀 예외핸들러에 의한 예외발생 시 예외처리 핸들러
    • Webclient예외에 의한 예외발생 시 예외처리 핸들러
    • DB연결 혹은 트랜잭션 예외발생 시 예외처리 핸들러
  12. ENUM 값에 대한 입력값을 받는 경우, CustomJsonDeserializer 을 구현하여 입력문자열 간 변환을 지원하였다.

    아래를 참고하였다. (길게 설명하면 귀찮다,,,)

    [ Spring ] Request Param을 enum으로 받기

  13. Entity 에서 ENUM 을 사용하는 경우, @AttributeOverride 를 적용했다.

    DB의 코드값과 Java Enum을 연결해주는 과정에서 @Convert를 사용하면 코드의 유지보수나 유연성을 확보할 수 있다.

    자세한 건 아래에서,,
    Overriding Column Definition With @AttributeOverride

  14. Entity 에서 ENUM 을 사용하는 경우, DB 값과 ENUM 에 대한 Converter 를 공통abstract구현체로 추출하여 처리하였다.

    대부분의 Converter 로직들은 데이터베이스에서 조회 한 코드로 Enum안에 있는 코드들을 찾아서 반환 시키는 반복적인 로직이다.

    따라서 이걸 좀 일반화 할수는 없을까? 라는 관점에서 공통abstract 구현체로 분리할 수 있다.

    분리한 뒤, enum 에 대해서 간단한 코드만 작성해주면,

    Converter 에 대한 Helper Class 를 일일이 만들 필요가 없어진다!

    아래를 참고하였다. (이것도,,,길게 설명하면 귀찮다,,,)

    JPA Entity Enum Converter를 좀 더 유연하게 관리하는 방법

    Converter 에 대한 기본틀은 아래를 참고했다.

    [Spring] JPA Enum Converter를 사용하여 Entity Mapping하기(Feat. Parameter value [~] did not match expected type Error)

    Legacy DB의 JPA Entity Mapping (Enum Converter 편) | 우아한형제들 기술블로그

  15. 패키지에 대해 windows 10 기준 cmd 창에서의 tree 커맨드를 통해 패키지 구조를 추출하였다.

    이곳저곳 찾아보니 이상한 명령문이 많았다,,,, (가령 cmd //a tree //f 라던지,,,등등)

    말 그대로 tree만 쳐주면 되는 일이었다,,,^^

  16. ddl-auto 는 “무조건 none” 으로,,, mysqldump 는 주기적으로,,,,

    아 기억해내고 싶지도 않다,,,,

    엔티티를 구성하다보니 생각조차 안 하고 create 로 놓고 실행을 돌려버린 사고가 있었다,,,,,

    아차차,,,,했으나 결국 실행이 되었고,

    멈추지 않는 폭주기관차는 기업에서 제공해준 데이터를 한 방에 다 날려주었다,,,,

    로그에 접근하려하니 관리자가 아니어서 denied 가 나서 binlog를 통한 복구조차 안 되었다,,,

    결국 기업에 직접 문의를 하였고, 이후에는 none 을 무조건적으로 사용했고, 주기적으로 mysqldump 처리를 하였다,,,, ㅠㅠ

꿀 빨며 애용했던 사이트는 https://www.tabnine.com/code/java/methods/ 라는 곳이 요긴하게 사용한 사이트이다.

특정 기술이나 프레임워크를 검색하면 예시 코드가 나오는데,

어떤 경우 수준급의 코드가 나와서 이삭줍는 여인들마냥 최대한 허리 굽혀 주워담았다.

무엇이 가장 어려웠는가?


아무래도 각방에 갇혀서 외부와의 소통을 차단한 채 코드를 구현하다보니, 내 관점에서밖에 보이지 않는 게 제일 컸다.

우회해서 코드리뷰와 조언을 받을 수 있도록 하는 게 최선이었다.

내 경험상 어떻게서든 유출하지 않는 선에서 베스트옵션을 디깅하는 수밖에 없는 것 같다.

구글링을 통해 블로그 포스트를 뒤지든, 같이 취준하는 사람들이나 단톡방에 조언을 구하든 말이다.

나는 두 가지 모두 활용했다.

덕분에 더 위에서 바라볼 수 있었고, 개선점들을 찾을 수 있었다.

그리고 끊임없이 내 코드를 의심하는 자세 또한 굉장히 중요하다고 생각한다.

“과연 책임과 역할 분리가 잘 되어있는가?”

“과연 이 구현이 최선의 옵션인가?”

“평일 오후 3시쯤 어디 한 번 어떻게 짯는지 뜯어나볼까? 라는 마음으로 코드를 들여다봤을 때, 과연 나는 평가항목에서 안 깎일 자신이 있는가?”

앞으로 어떤 공부를 할 것인가?


뭐,,,

사실 생각이 없다.

내가 약한 부분을 꾸준히 발전해나가는 게 중요하다고 본다.

이번에 조언을 들으면서 아래 공부가 부족한 것 같아 더 공부해보려고 한다.

  • 모듈로 구현해보기
  • WebClient 와 WebFlux 에 대한 깊은 이해도
  • 어떻게 하면 스프링부트에서 Performance Bottleneck 에 대한 Profiling 을 처리할 수 있을까?
  • 코틀린으로 짜보기
  • MSA 공부는 그냥 하자
    • 지금 안 쓰이더라도 기업에서 쓰고 있으니 공부 안 할 이유가 없음
    • 해서 나쁠 게 없음
  • 도커, 컴포즈, 쿠버네티스
  • 꾸준히 코테는 풀자
profile
내 코드로 세상이 더 나은 방향으로 나아갈 수 있기를

2개의 댓글

comment-user-thumbnail
2024년 4월 11일

잘 읽었습니다!

답글 달기
comment-user-thumbnail
2024년 4월 16일

많은 도움이 되었습니다 !

답글 달기