본문 바로가기
프로젝트 회고/주차별 회고

이노베이션 3,4,5 주특기 회고

by 구너드 2023. 7. 13.

3,4,5주차는 스프링을 활용하여 블로그를 점진적으로 발전해나가는 방식으로 진행되었다. 처음에는 단순히 CRUD를 구현하는 것부터 시작해서 로그인, 댓글, 권한부여 기능을 하나씩 추가하면서 보다 완성도 있게 서버를 만들어나갔다.


<해당 주차 결과물>
 
https://github.com/Goonerd17/ec2

 

GitHub - Goonerd17/ec2

Contribute to Goonerd17/ec2 development by creating an account on GitHub.

github.com


처음에는 막연하게 어떤 것부터 시작해야할지 고민했지만 작은 기능들부터 구현해나가면서 완성시킬 수 있었던 점이 마음에 들었던 주차였다. 지금도 많이 느끼지만 스스로의 부족함을 알고 있었고 내가 할 수 있을까라는 의문이 들었다. 다만 부족한 걸 알면서도 아무것도 시도하지 않으면 끝까지 부족한 상태로 남을 수 밖에 없다는 걸 알았기 때문에 코드 작성 과정 중 모르는 것이 나와도 최대한 고민하고 스스로 해결해보려고 노력했던 것 같다.


이번 주특기 주차를 진행하면서 고민했던 부분들
 
1. 인증, 인가 부분의 반복성 해결
 
jwt 토큰을 Service 계층에서 검증하는 것은 코드의 반복을 발생시킬 것이라고 예상했다. 그리고 이러한 반복은 분명 오탈자, 적용과정에서의 헷갈림을 초래해서 굉장히 피곤한 코드가 될 것 같았다. 그래서 필터, 인터셉터에 대해서 얕지만 어느 정도의 개념은 가지고 있었기 때문에 해당 개념들을 사용하려했다. 막연하게 인증 부분의 반복 문제를 고민하는 과정에서 스프링 시큐리티를 알게 되었다. 실제로 컨트롤러에 요청이 들어오기 전에 인증, 인가 부분을 일괄적으로 처리해주는 프레임워크라는 점은 매우 매력적으로 느껴졌고 이를 실제로 적용해보고 싶었다. 
 
처음에는 스프링 시큐리티 개념에 대해서 이해하기 어려웠다. 생소한 단어들도 많이 나오고 인증과 인가의 프로세스를 한 번에 이해하기에는 역량이 부족했다. 그럼에도 정해진 시간 내에 과제를 진행하기 위해서 부끄럽지만, 강의에서 제공하는 코드를 복사하여 차용하였다. 물론 이런 코드 작성법이 내게 아무런 도움이 되지 않다는 사실은 알 수 있었다. 그래서 코드 자체를 복사하여 과제를 진행하는 시간은 줄이고 복사한 코드들을 하나씩 분석해가며 원리를 이해해가는 방식으로 학습법을 변경했다. 그리고 이러한 부분은 Filter에서 발생하는 예외를 @ControllerAdvice로 처리하는 게 아니라 예외처리를 하는 새로운 Filter를 생성, @GetMapping으로 요청받는 URL은 비로그인 사용자도 접근 가능과 같은 변경된 요구사항에서 어떠한 부분을 수정해야 해당 변경사항들을 적용할 수 있는지 파악하는 데 도움이 되었다.
 
아직은 스프링 시큐리티가 인증 처리 과정에서 UsernameAndPasswordAuthenticationToken이 어떠한 프로세스를 거쳐 로그인 과정에서 SecurityContextHolder에 저장되는지 정확하게 이해를 못했다는 점이다. 보다 정확하게 설명하자면 로그인한 사용자의 인증정보를 UserServiceDetailImpl을 통해 DB에서 조회 후, 일치한 사용자가 있다면 이를 SecurityContextHolder에 어떻게 저장되며 스프링 시큐리티는 인증된 사용자에게만 인가되는 URL을 들어갈 때 어떠한 방식으로 해당 인증정보를 활용하는지에 대해서는 이해가 부족한 것 같다. 아마 해당 부분은 시간이 날 때, 스프링 카테고리에서 좀 더 자세히 다루고 싶다 
 
저번에 개인적으로 공부했을 때 느꼈던 Filter와 Interceptor의 차이점에 대해서도 생각해볼 수 있었다. request 객체의 다운 캐스팅 여부, 인가 URL 지정 방식 차이, 각각 적용되는 시점에서도 둘의 차이점은 명확하지만 무엇보다 가장 큰 차이점은 특정 기술에 대한 종속성이라고 느꼈다. Filter는 서블릿에, Interceptor는 스프링에 종속적이다. 이 부분에 대해서 같이 공부하는 분과 이야기를 했는데, 어쨌든 스프링을 활용하는 입장에서는 Interceptor를 활용하는 게 좀 더 알맞는 방향성이 아닐까 싶었다. 물론 둘 다 활용하는 방안도 좋겠지만 좀 더 구현해보고 싶다는 느낌이 든 건 Interceptor였다. 다만 스프링 시큐리티를 활용하고 과제의 기한에 맞게 코드를 작성하다보니 해당 부분에 대해서 생각만 하고 실제로 구현해보지 못했다는 점이 아쉽다. 나중에 기회가 된다면 시큐리티를 사용하지 않고 Interceptor만을 가지고 이번 과제를 재구성해보고 싶다.


2. 조회에서 얻을 수 있는 성능적 이점
 
Comment를 최근 등록순으로부터 상단에 위치하도록 정렬하는 요구사항이 있었다. 단순히 하나의 엔티티를 정렬하기 위해서는 쿼리메서드를 이용하여 구현하는 것이 쉬웠지만 Post와 연관관계를 맺고 있는 Comment의 경우에는 어떻게 해당 기준을 구현할지 고민이 되었다. 처음에 생각했던 것은 PostResponseDto에서 stream() 을 사용해서sorted(collection,reverseOrder())를 이용하는 방법이었다. 해당 부분은 CommentResponseDto가 Comparable을 구현하고 정렬기준 메서드를 오버라이딩해야했지만 그렇게 복잡한 작업도 아니었고 원하는대로 결과도 나왔기에 만족스러웠다. 하지만 시간이 지난 후 다시 코드를 검토할 때, CommentList의 타입이 탐색, 조회에는 우수한 성능을 보이지만, 중간 데이터의 수정과 변경에는 비교적 느린 성능을 보이는 ArrayList이기 때문에 해당 부분을 sorted를 이용해 애플리케이션 차원에서 재정렬하는 것은 생각보다 많은 리소스 소모와 성능에서의 약점을 보일 수 있을 것 같다는 생각이 들었다.
 
해당 부분에 대해서 같이 공부하는 분께 조언을 구했는데 DB에서 직접 역순정렬된 데이터를 가져오는 게 더 좋을 것 같다는 피드백을 받았다. 피드백을 받으면서도 내가 놓쳤던 부분이 아쉽긴 했지만 그 과정이 있었기에 좀 더 고민을 하며 코드를 작성할 수 있었던 것 같다. 이후에 해당 문제에 대해서 기술매니저님과 이야기를 해봤는데 그렇게 큰 성능적 이점은 없기에 변경사항이 생기면 애플리케이션에서 로직을 빠르게 수정할 수 있도록 해주는 Comparable 방식도 나쁘지 않은 것 같다고 피드백해주셨다.


3. fetch join과 batchsize
 
2번의 문제를 fetch join을 통해 해결하였다. 기존에 댓글이 셀 수 없이 많은 Post일 경우를 산정하지 않았기 때문에 n + 1 문제를 간과하고 있었는데 2번을 기반으로 리팩토링을 진행하면서 n + 1의 문제를 같이 고민할 수 있었다. 이를 해결하기 위해서 fetch join을 이용하여 이전보다 코드도 깔끔하고, 아마 미미하지만 애플리케이션 차원에서 정렬하는 것보다 성능이 향상되었을 것이라고 기대를 할 수 있었다. 다만 과제 제출 기한까지 고민이 되었던 점은 과연 fetch join이 적절한 선택이었을까라는 점이다.
 
공부한 내용을 떠올려보면 fetch join은 매우 좋은 기술이지만 컬렉션 한정으로 치명적인 단점이 있다. 바로 페이징을 사용할 수 없다는 점이다. fetch join을 사용하면 기대하는 테이블의 row 수보다 더 많은 row가 생성될 수 있기 때문에 페이징 사용은 불가하다. 만약 댓글에서도 페이징 기능이 적용이 되었다면 fetch join이 아닌 BatchSize를 고려해야한다. 요구사항에는 페이징 기능이 적혀져 있지 않기도 하고, 일반적으로 댓글에 대한 페이지가 필요할까라는 생각을 했지만 두 상황 모두 가정해보고 추가적으로 과제를 완성도 있게 구성해봤으면 더 좋은 프로젝트가 되지 않았을까라는 아쉬운 점이 있다. 나중에 프로젝트를 진행하면서도 혹시나 하는 상황을 염두에 두고 코드를 작성하는 점이 좋을 것 같다고 느낄 수 있었다.


4. 비즈니스 로직의 위치?
 
'도메인에 비즈니스 로직을 맡긴다' 라는 개념은 처음에 들었을 때 굉장히 신선했다. 이러한 부분을 염두에 두고 꾸준하게 코드를 리팩토링 했던 것 같다. 보다 정확하게는 '도메인이 특정 조건에 따라 자신의 상태를 유연하게 변경하고 이를 통해 외부에서는 쉽게 도메인의 상태를 변경하지 못하도록 설계하자' 라는 생각으로 리팩토링에 임했던 것 같다. 객체지향을 공부하면서 가장 크게 재밌었던 점은 객체들은 다른 객체가 어떤 역할과 속성을 지니고 있는지 알지 못한채 자신에게 메세지가 온다면 그저 그 메세지를 수행할 뿐이라는 점이었다. 막연하게만 느껴졌던 해당 개념은 DI에서 어떤 느낌인지 감을 잡을 수 있었다. 그리고 이런 개념을 도메인에게 적용하면 보다 유지보수에 용이한 설계가 되지 않을까라는 생각을 했다. Service 계층의 특정 로직은 반복되고 있었기 때문에 해당 부분을 도메인에 할당하면서 유저 일치와 권한에 따라 수정 여부를 도메인 스스로가 결정할 수 있게끔 만들었고 결과는 꽤 만족스러웠던 것 같다. 정확히 어떤 역할을 하는 도메인 메서드인지 알려주기 위해 메서드 이름을 고민하는 과정부터 어떤 조건식으로 작성하면 보다 한눈에 이해하기 쉬울까라는 고민들을 하면서 완성도 있는 코드를 추구했다. 
 
이 후 시간이 지난 뒤 코드들을 검토해보는데 도메인에 비즈니스 로직을 넣은 시도 자체는 좋았지만 결국에 해당 비즈니스 로직은 도메인 내부에서도 반복됨을 알 수 있었다. 원래 가장 중요한 목적은 반복을 없애는 것이었는데 내가 리팩토링한 결과물은 단순히 비즈니스 로직을 도메인 안에 넣기만 했을 뿐 실제 중요한 목적은 해결하지 못했다. 고민을 하면서 boolean 반환타입으로 조건 일치 여부만 도메인 내부에 넣으려고 했으나 이는 도메인이 메세지를 수신한 뒤, 조건 판단을 하는 역할만 할 뿐  추가적인 상태변경은 Service 계층에 맡기는 모양이 되었다. 이러한 방식은 내가 의도했던 설계가 아니었고 아쉽지만 도메인에 비즈니스 로직을 넣는 방식은 실패로 끝났다. 
 
기존 방식에 얽매이지 않고 여러 방식으로 코드를 작성해보려고 했던 점은 좋은 시도였던 것 같다. 또 그 과정에서 하나의 특정 Service 계층에서 실행되는 공통 로직은 해당 Service 계층에서 공통 메서드로 추출하고, 여러 Service 계층에서 일관적으로 사용되는 로직들은 도메인 내부로 넣는 게 좀 더 도메인 모델을 사용하기 적합한 환경이 아닐까라는 추가적인 고민도 할 수 있었다. 아마 이 부분에 대해서 도메인 주도 설계를 공부하면 좀 더 확실하게 답을 알 수 있지 않을까 싶다. 물론 아직은 해당 개념에 대해서 공부하진 못했지만 기회가 된다면 해당 부분도 추가적으로 알아보고 싶다. 


공부를 하면 할 수록 알아가는 것도 많지만, 모르는 것도 점점 늘어난다. 그래도 모른다는 사실조차 모르는 것보다 내가 모르는 것이 무엇인지 알 수 있다는 점이 훨씬 좋은 것 같다. 다만 요즘에는 근본적인 질문도 꽤 던지게 되는 것 같다.
 
예시로 들기 되게 좋은 것은 종속성인 것 같다. 최근에 배운 스프링의 예외처리 추상화는 DB 접근 기술마다 다르게 발생할 수 있는 예외들을 Checked Exception이 아닌 Runtime Exception으로 한 번 감싸서 처리한다. 왜냐하면 예외처리를 강제하거나 thorws 를 명시적으로 적어야하는 Checked Exception의 경우에는 해당 예외와 관련도 없고, 처리할 수도 없는 계층에게까지 영향을 미친다. 결국 해당 예외가 만약 수정된다면 그와 관련된 모든 계층들도 throws 에 명시적으로 적혀져 있는 예외들을 변환해야하고 이는 결국 특정 계층들이 해당 예외에 종속적이기 때문에 발생하는 일이다.
 
뭔가 주제가 좀 엇나간 거 같은데 내가 느끼는 점은 종속성이란 개념과 유사한 것 같다. 결국 코드를 작성하는 것만 열심히 해서는 내가 코드에 종속적인 느낌을 받는다. 코딩과 프로그래밍은 완전히 다른 것이라는 걸 시간이 지날수록 느껴지기 때문에 스스로에 대한 부족함도 그만큼 커질 수 밖에 없다. 단순히 코드를 잘 작성한다를 넘어서 어떤 특정 사안을 파악하고, 해당 사안을 어떤 설계로 구성할지, 각 설계에서 얻을 수 있는 이점과 단점은 무엇인지, 그렇다면 어떤 근거와 판단으로 설계를 선택하고 코드를 어떻게 작성할 것인지 고민하는 과정은 많이 부족한 것 같다. 이번 과제에서도 결국 내가 고민한 것은 해당 과제의 코드들을 어떤 식으로 리팩토링할 것인지에 대해서 고민했을 뿐, 과제에 대한 전반적인 이해와 추가적으로 고려해볼만한 사항들은 뒤늦게 깨달았다.
 
단순히 코드의 반복을 줄이고 간결한 코드를 작성하는 작업도 중요하긴 하다. 결국 그러한 기초가 탄탄히 쌓여야 내가 고민하고 있는 문제들에 대해 어떤 선택을 할 때, 나름대로 합리적인 근거를 가질 수 있을 것이다. 다만 지금 코드를 잘 빠르게 작성하고, 기술과 관련된 많은 학습량이 프로그래밍 실력향상으로 이어진다고 생각하는 것은 되게 위험한 것 같다. 개인적인 욕심때문에 최대한 빠르게 스프링 기술을 습득하고 노력했지만, 결국 해당 기술들은 스프링의 기술일 뿐, 전체적인 설계를 구성하고 계획하는 능력은 별개라는 걸 많이 느꼈다.  물론 지금 내가 프로젝트 대비를 위해서 할 수 있는 것이 스프링과 관련된 기술 공부였기 때문에 그 방식이 잘못됐다라고는 생각하지 않는다.  하지만 이러한 방식과 별개로, 지금까지 내가 배운 것들은 정말 작은 부분들이고, 진짜 프로그래밍을 잘하기 위해서는 컴퓨터 기초 지식부터, 자바와 그 외의 프로그래밍 언어들, 아키텍처, 문제 파악능력들이 필요하다는 걸 많이 느낀다. 정리하자면, 어떤 특정 기술을 바꿔쓰게 된다면 처음부터 그 기술을 배워야 하는 게 아닌, 기초적인 것들에 대해서 탄탄히 배우고, '프로그래밍에 대한 전반적인 흐름을 파악하고 싶다'라는 앎의 욕구가 점차 강해지고 있다. 마치 내가 코드에 종속적인 상황에서 코드가 내게 종속적인 상황으로 바꿀 수 있게끔 말이다.
 
뭔가 글을 적다보니 쓸데없이 길어지고 정확히 하고 싶은 말에 대해서는 못 적은 것 같다. 글쓰기도 진짜 쉽지 않다.. 어쨌든 공부를 하면 할 수록 느끼는 점은 점점 알아가야할 게 많다는 점이다. 단순히 코드를 작성하고 프로젝트를 잘 만드는 것도 알아야하지만 보다 근본적으로 코드에 대한 이해, 설계 방식, 컴퓨터 구조, 코드를 통해 작동되는 프로그램의 원리에 대해서 꾸준히 공부하고 싶다. 이는 앞으로 살면서 장기적으로 가져가야할 욕심들이지만, 포기하지 않고 잘 해내고 싶다.