본문으로 건너뛰기

자프링 학습 회고

· 약 6분
brown
Frontend Developer

이제 날이 살살 풀리는데 올해 여름은 참 더웠다.

6월부터 자프링 공부를 전업으로 진행 했는데, 더운 시기에 실내에서 공부만 해도 됐다는건 감사한 일인 것 같다.

공부한 내용들에 대한 간단정리 및 회고를 작성해본다.

왜 자바 스프링을 공부했는지

이전부터 혼자서도 웹 서비스를 제대로 구축할 수 있는 역량을 갖추고 싶어 백엔드 공부를 생각은 하고 있었다.

원래는 상대적으로 익숙한 node계열로 하게 되지 않을까 생각했는데, 스프링으로 수련하게 되었다.

계기는 퇴사 직전 한달 정도 spring 프로젝트에 api 몇개 만들었는데, 생각보다 할만하다고 느꼈다.

웹 백엔드 쪽에서 자바 스프링이 가장 보편적으로 쓰이고 학습자료도 많지만,

러닝커브 때문에 선뜻 손이 가지 않았는데 마침 시간도 나고 할만하다고 느낀김에 공부를 시작했다.

어떻게 공부를 했는지

김영한님의 강의를 학습하는 방식으로 진행중이다. 심플한 이유인데, 만나본 서버 개발자분들이 추천했다.

그만큼 실제로 많은 개발자들이 강의를 듣고, 강의에서 나오는 내용은 기본으로 깔고 가겠구나 싶어서

김영한님의 커리큘럼을 진행하고 있다. 현재 7편정도 완강했는데, 영한님을 너무 많이봐서 내적 친밀감이 생겨버렸다.

강의에 대해

내가 느낀 김영한님의 강의 진행 방식은 어떤 기술이나 개념이 과거부터 현재까지 어떻게 발전했는지를 처음부터 현재까지 쌓아나가는 방식이다.

장점은 이해하기가 쉽다는 것이고, 단점은 연속해서 강의를 듣는 경우에는 지칠 수 있다는 점이다.

나중에는 현재 쓰이는 베스트 프렉티스만 보고싶다는 생각을 하기도 했다.

그래도 지식적인 부분이나 이해하는 부분에서 강점을 보이는 방식이다.

그리고 강의 하나 완강하는데 적힌 학습 시간의 2 ~ 3배는 걸리는 것 같다.

자바 관련

자바 코드가 실행되는 과정

자바 프로젝트는 Maven, Gradle같은 툴로 관리하는데 node의 패키지 매니저와 유사하다.

Gradle의 빌드의 라이프사이클은 3단계로 이루어진다.

  1. 초기화 (Initialization) - settings.gradle 파일을 읽어 빌드에 포함될 프로젝트들을 결정

  2. 구성 (Configuration)

    • 의존성 설치
    • build.gradle 스크립트를 실행하여 정의된 모든 태스크(Task)와 실행 순서를 담은 계획도(Task Graph)를 메모리에 생성
  3. 실행 (Execution) - 계획한 작업(Task)들을 실제로 실행

    • plugins에 java, spring등 항목들이 필수적인 태스크를 자동으로 생성해준다.
    • clean: build 디렉터리를 삭제하여 이전 빌드 결과물을 모두 정리합니다.
    • compileJava: 자바 소스 코드를 컴파일하여 .class 파일을 생성
    • test: 테스트 코드를 실행하고, 결과 리포트를 build/reports에 생성
    • build: compileJava, test, jar 등 빌드에 필요한 여러 태스크들을 순서대로 모두 실행하는 태스크
    • bootJar: 모든 의존성 라이브러리까지 포함된 실행 가능한(executable) Fat JAR를 생성(spring에서 build 태스크 실행 시 기본 jar 대신 이 태스크가 동작)

JVM의 클래스 실행 과정은 크게 로딩, 링킹, 초기화, 실행의 4단계로 나뉜다.

  1. 로딩(Loading): 클래스 로더가 .class 파일을 찾아 JVM 메모리에 올리는 단계

  2. 링킹(Linking): 메모리에 올라온 바이트코드를 실행 가능한 상태로 만드는 과정

    1. 검증(Verification): 바이트코드가 유효한지
    2. 준비(Preparation): static 변수들을 위한 메모리를 할당 및 기본값 초기화
    3. 분석(Resolution): 코드에 사용된 심볼릭 참조를 실제 메모리 주소로 바꾸는 작업
  3. 초기화(Initialization): 스태틱 변수 실제 값 할당 및 static {} 실행

  4. 실행(Execution): 코드를 실행

jar 파일은 thin jar(프로젝트 코드만), fat jar(모든 의존성 포함)로 나뉜다.

위 실행 과정은 프로젝트의 메인 함수를 대상으로 진행한다.

JVM은 thin jar의 프로젝트의 메인 함수를 로딩하는데, 외부 의존성을 Class Loader가 메모리에 올릴 수 있게 Classpath(경로)를 명령어에 포함해야 한다.

반면 spring 프로젝트의 fat jar의 경우, 클래스 로더가 JarLauncher을 실행하고

런처가 커스텀 클래스로더를 생성해 내부의 외부 의존 jar를을 로딩하고 프로젝트의 메인함수를 실행한다.

컴파일타임에 코드를 생성할 수 있다.

Lombok이나 queryDsl 같은 라이브러리들은 어노테이션에 따라 컴파일타임에 코드를 생성한다.

이부분은 마법처럼 느껴졌다.

어노테이션 프로세서(Annotation Processor)는 어노테이션을 보고, 필요한 코드를 컴파일 중인 .class 파일에 직접 추가한다고 한다.

이것은 런타임에 동작하는 리플렉션(Reflection)과 달리, 빌드 시점에 모든 작업이 끝나므로 성능 저하가 없다.

리플렉션은 런타임중에 클래스 이름만으로 객체를 생성하고, 그 객체의 메서드나 필드에 접근할 수 있게 하는 기능이다.

다형성 참조

A 클래스에 value 프로퍼티가 있고, B 클래스는 A를 상속 받았다.

B 클래스로 객체를 생성 했을 때, 해당 객체의 value 프로퍼티를 두개로 관리 할 수 있다는 것이다.

같은 객체의 같은 프로퍼티여도 해당 객체의 타입에 따라 값이 다를 수 있다.

이 개념은 정말 신기 했다.

인터페이스가 다른점

java의 인터페이스는 인스턴스가 가져야 할 프로퍼티를 정의 할 수 없다.

이 사실을 알기전까지는 추상클래스의 필요성에 대해 잘 이해하지 못했다.

추상 클래스는 관련성이 매우 높은 클래스들 간에 공통된 '상태(멤버 변수)'와 '구현(메서드)'을 공유하고 싶을 때 사용합니다. 인터페이스가 '무엇을 할 수 있는가(can-do)'를 정의한다면, 추상 클래스는 '무엇인가(is-a)'라는 공통된 정체성과 뼈대(Skeletal Implementation)를 제공하는 데 목적이 있습니다.

서블릿 컨테이너에 대해

node진영은 http 모듈을 기반으로 WAS를 만든다.

java 역시 node처럼 기본적인 HTTP 서버 기능을 내장하고 있지만, 자바 생태계는 서블릿(Servlet) 기술을 중심으로 발전했다.

서블릿은 명세이고 서블릿을 지원하는 WAS를 서블릿 컨테이너(톰캣, Jetty..)라고 한다.

서블릿 컨테이너는 @WebServlet어노테이션을 찾아 해당 객체를 생성하고, URL 주소와 이 서블릿 객체를 내부 맵(Map)에 등록한다.

그리고 객체의 메서드를 호출하여 요청을 처리하도록 위임한다.

스프링 관련

스프링은 대규모 앱 개발을 위한 프레임워크고, boot는 최소한의 설정으로 사용할 수 있도록 만들어 준다.

스프링 DI 컨테이너는 객체의 생성 및 의존 관계 연결 책임을 개발자 코드 밖으로 옮겨, 클라이언트 코드를 변경하지 않고도 구현체를 교체할 수 있게 한다.

요즘 개발에는 spring boot + spring data JPA + queryDsl 조합이 주로 사용된다 한다.

스프링이 실행되는 순서

핵심 내용만 축약해서 작성했다.

  1. SpringApplication.run 이 실행된다.
  2. 환경 정보를 로드
  3. 스프링 컨테이너 ApplicationContext 생성
    • 서블릿 웹: AnnotationConfigServletWebServerApplicationContext
    • 리액티브: ReactiveWebServerApplicationContext
    • 일반(non-web): AnnotationConfigApplicationContext
  4. context.refresh() 메서드 호출
    1. Bean Factory 생성
    2. @ComponentScan 실행
    3. 빈(Bean) 생성 및 의존성 주입
  5. 서버 구동

찾아보면 관련한 내용 중에 상세한 작성된 글들이 많다.

통신의 라이프사이클

  1. 클라이언트의 요청
  2. 서블릿 컨테이너가 요청을 받음
  3. 서블릿 필터 체인 실행
  4. 쓰레드를 할당해 매핑된 서블릿의 메서드 실행
    • 스프링은 DispatcherServlet이라는 하나의 서블릿만 사용(프론트 컨트롤러 패턴)
    • DispatcherServlet이 컨트롤러 메서드를 찾음
  5. 스프링 인터셉터
    • preHandle: 컨트롤러 호출 전에 호출된다.
    • postHandle: 컨트롤러에서 예외가 발생하면 호출되지 않는다.
    • afterCompletion: 예외가 발생해도 호출 되는 것을 보장
  6. 컨트롤러
    1. argument resolver들이 request를 메서드의 인자로 변환
    2. 컨트롤러 메서드가 실행되어 결과값 반환
      • @ResponseBody의 경우 HttpMessageConverter가 객체를 JSON 문자열로 변환
      • view를 반환 시 물리 경로의 View 객체로 변환 -> HTML 응답을 생성
  7. 이후 역순으로 클라이언트에게 응답이 반환된다.

에러 처리

컨트롤러에서 발생한 예외가 스프링 내부에서 처리되지 않고 밖으로 던져지면, 그 예외는 필터를 거쳐 최종적으로 서블릿 컨테이너(톰캣)까지 전파된다.

그러면 서블릿 컨테이너에서 에러경로로 다시 요청을 보낸다.

하지만 스프링에서는 DispatcherServlet에서 등록된 HandlerExceptionResolver로 에러를 잡아 내부에서 처리하기에 재요청이 발생하지 않는다.

커스텀으로 에러를 처리하려면 HandlerExceptionResolver를 구현하기 보다는

@ExceptionHandler@ControllerAdvice를 사용하면 ExceptionHandlerExceptionResolver가 처리해준다.

후기

내용이 방대한데 가장 핵심적인 내용이라고 느끼는 부분만 추렸다.

쓰면서도 내가 명확하게 알지 못했구나 싶었던 부분들이 계속 생겨 작성하는데 오래 걸렸다.

그래서 다음직무에는 서버쪽 일을 겸할 수 있을지는 잘모르겠다.

준비할 기회가 생긴 김에 준비하는 것이고, 잘되길 바랄뿐 ㅠㅠ!!!

회사를 다닐때는 풀타임으로 공부할 시간이 있으면 좋겠다는 생각을 많이 했는데, 막상 그럴 수 있었던 시간을 최대로 활용하지 못한 것에는 아쉬움이 남는다.