Changsoon's Note Backend Developer

스트림의 collect와 toList

alt text

인텔리제이의 스트림 구문에서 주의 표시가 나와서 확인해보니 .collect(Collectors.toList()) 대신 .toList로 사용하라는 문구가 나왔다.

왜 그런지 알아보자!

collect?

자바 스트림에서 collect 메서드는 스트림의 원소들을 수집해서, 원하는 컬렉션으로 반환하는 데 사용된다.

collect() 메서드는 종결 연산으로 스트림을 최종적으로 처리하여 결과를 반환한다.

또한, Collector 객체(스트림의 요소를 수집하는 방식을 정의하는 객체)를 인자로 받는다.

그냥 .collect() 를 사용하면 되지, 왜 toList()를 사용하라고 주의룰 줄까?

답은 간결성과 불변성에 있다.

간결성

Stream.toList()는 Collectors.toList()를 사용하는 것보다 더 간결하다.

Stream.collect(Collectors.toList())는 collect() 메서드를 사용하여 Collector를 명시적으로 제공하는 방식인데, Stream.toList()는 이러한 중간 단계를 생략하고 직접 리스트를 반환한다.

불변성

Stream.toList()는 반환된 리스트가 불변하도록 보장한다.

이는 리스트가 수정되지 않도록 강제하기 때문에 코드의 안정성을 높이는 데 도움을 준다.

반면, Collectors.toList()는 기본적으로 가변 리스트를 반환하기 때문에, 리스트를 수정할 수 있다.

실제로 코드를 까보면 unmodifiableList로 불변 리스트를 반환한다.

alt text

그러므로 IDE에서는 간결성과 불변성을 제공하는 toList()를 사용하기를 권장하는 것이다.

로컬 성공, 배포 후 문제

프로젝트 개발 중 Spring Batch에서 데이터를 가져온 뒤 CSV 파일에 저장하는 코드가 있었다.

로컬에서 테스트를 하고 잘 작동하는 것을 확인한 뒤 배포를 하니 문제가 발생했다.

로컬에서와 배포 서버의 경로 차이가 원인이었다.

로컬에서 개발을 하고 경로를 지정하여 빌드 후 실행해보면 해당 경로를 잘 찾는 것을 확인했지만 배포 후에는 경로가 달라지므로 상대 경로로 접근하는 것보다 절대 경로를 사용하는 것이 안전하다.

예를 들어, jar 파일로 빌드하여 배포하면 jar 파일의 루트 경로는 jar:file:/이 된다.

로컬에서는 file:/ 경로로 찾는다.

그렇기 때문에 클래스패스(classpath)를 통해 자원에 접근해야 한다.

로컬 환경에서 잘 작동하던 코드가 배포 환경에서는 권한을 요구하는 경우가 생길 수 있다.

예를 들면, CI/CD 배포 스크립트에서 gradlew의 권한이 없어서 실행하지 못하는 경우가 있을 수 있다.

그럴 땐 배포 스크립트에 권한을 부여하는 코드를 추가해주자.

멀티모듈을 왜 적용했는가?

이전에 회사 프로젝트에 멀티 모듈을 적용하면서 알아봤던 내용과 왜 적용했는지를 정리해보자.

멀티모듈과 첫 만남

처음 멀티모듈이라는 것을 알게된 것은 공부 차 오픈소스를 둘러보던 중이었다.

당시에는 단일 모듈 프로젝트 이외엔 경험해보지 못했기 때문에, 조금 복잡한 구조네.. 싶었던 기억이 있다.

조금 더 코드를 둘러보고 Maven이나 Gradle과 같은 빌드 툴을 이용해 하나의 프로젝트에 여러 모듈을 적용한거구나! 라고 단순하게 생각했었다.

하지만 단순히 여러 프로젝트를 하나의 프로젝트로 모아 놓은 구조만은 아닌 것이라는 것을 시간이 좀 지나고 알게 되었다.

멀티모듈이란?

그렇다면 단순히 모듈을 모아놓은 구조만이 아니라면 멀티모듈은 무엇일까?

멀티 모듈은 주로 소프트웨어 개발에서 하나의 프로젝트를 여러 개의 독립적인 모듈로 나누어 관리하는 방법을 말한다.

GPT에게 정의를 물어보니 이렇게 답한다.

GPT의 정의에서 “독립적인 모듈을 나누어”라는 문구에서 알 수 있는 단순히 분리한다는 개념뿐만 아니라, 모듈을 한 번에 관리하는 것에 좀 더 비중이 있다고 생각한다.

단일 모듈의 문제점

멀티 모듈이 아닌 단일 모듈의 문제점을 알아보자.

서비스가 커지면서 관리자페이지에 서비스의 상태를 모니터링하는 기능이나, 애플리케이션을 관리하는 기능의 추가가 필요해지는 경우가 생겼다.

문제는 단일 모듈 프로젝트에서, 관리자페이지에 기능을 추가해서 배포하려면 애플리케이션에 작동하는 코드까지 함께 배포가 되고 그에 따라 크고 작은 영향을 주게 된다는 것이다.

예를 들어, 관리자페이지에 사용자의 컨텐츠를 조회하는 기능에 다른 정보를 추가해서 가져오게 되면 해당 코드를 직간접적으로 사용하고 있던 애플리케이션 API는 영향이 갈 수 있다.

이를 인지하고 수정하면 괜찮지만, 그렇지 않은 경우에 문제가 생긴다.

코드 뿐만이 아니라 배포시에도 문제가 발생한다.

예를 들어, 관리자페이지에서만 발생한 테스트 실패로 인한 빌드 실패가 테스트 코드가 모두 통과한 애플리케이션 코드까지 실패하게 된다.

이러한 문제를 해결하기 위해 관리자와 애플리케이션의 코드를 분리할 필요성을 운영진, 개발자가 모두 느끼고 있었다.

멀티모듈을 적용한 이유

먼저 해당 문제를 해결할 가장 잘 알려진 방법은 마이크로서비스 아키텍처(MSA)를 적용하는 것이다.

실제로 이 방법을 먼저 적용하려고 시도했었다.

하지만 현재 프로젝트에 MSA는 맞지 않다고 결론이 났는데, 이유는 다음과 같다.

  1. 회사 프로젝트가 MSA를 적용할 정도로 규모가 크지 않다.
  • 회사 프로젝트는 단일 모듈을 적용해도 큰 문제는 없고, 프로젝트 백엔드 개발도 혼자서 하기 때문에 MSA로 하게 될 때는 관리 포인트가 늘어나고, 오히려 이러한 구조가 개발 문제 발생을 초래할 것으로 예상되었다.
  1. MSA에 대한 지식 부족
  • 저 문제에 대해 고민하던 당시엔, Spring Cloud나 K8S와 같은 MSA를 쉽게 구축할 수 있는 기술에 대한 지식도 부족했고, MSA 자체에 대한 지식도 거의 없는 상태였다.
  • 이러한 상태에서 무작정 MSA를 구현하게 되면 문제가 발생할 것이 분명했다.
  1. 분리된 코드 관리
  • 1번과 비슷한 문제인데, MSA를 적용하게 되었을 때 분리된 프로젝트 안에서 각 도메인 로직을 계속 같게 유지해줘야 하는데 해당 부분에 대한 관리를 혼자서 하기에는 문제가 발생할 것을 보였다.
  1. 비용 문제
  • 현재 단일 모듈 프로젝트는 AWS EC2에 dev 서버, prod 서버 두 개의 인스턴스에 각각 올라가 있는 상태였다.
  • 이러한 상황에서 MSA로 인한 인스턴스 증가는 결국 비용이 추가로 발생하는 것을 의미했고, 작은 규모의 스타트업에선 이것은 큰 부담이 되었다.

결국 리소스도 많이 필요하지 않고, 공통 코드는 잘 관리되며, 코드가 분리되어 확장성과 유지보수성을 가진 쉬운 구조가 필요했다.

이러한 문제들을 멀티 모듈 구조는 해결해준다.

  • 멀티 모듈 구조에서는 프로젝트를 크게 바꾸지 않고 어렵지 않게 전환이 가능하며, 관리하기 편하게 만들어준다.
  • 완벽히 분리된 구조를 만드려면, 인스턴스가 늘어나긴하지만 MSA로 전환했을 때의 리소스보단 크게 줄어든다.

멀티모듈 구조

간단하게 ADMIN, USER 이렇게 두 개의 도메인이 있다고 가정하고 생각해보자.

각각의 도메인은 presentation 영역, application 영역, domain 영역, infrastructure 영역으로 Layer로 구성되어 있었다.

alt text

이러한 Layer 구조에서 멀티모듈로 전환할 때 공통적으로 관리해야 하는 부분은 핵심 로직이 들어있는 domain 영역과 application 영역이 있다.

infrastructure 영역은 외부 기술(DB나 외부 API)를 사용하는 영역이다.

presentation 영역을 ADMIN-API과 USER-API 두 개의 모듈로 만들어 domain 모듈을 의존하는 방식을 사용했다.

이렇게 하면 ADMIN과 USER를 따로 배포할 수 있고 공통 코드는 관리되며 코드가 분리되어 유지보수하기 쉬워지는 구조를 만들 수 있게 된다.

멀티모듈 구조와 의존성 관계는 이렇다.

alt text

주의할 점은 각 모듈별에서만 사용할 의존성을 설정해야 한다.

예를 들어, infrastructure 모듈에서만 db 관련 의존성만 설정하고 나머지 모듈에서는 사용하면 안 된다.

다른 모듈에서도 사용할 수 있게 되면 모듈 간의 경계가 허물어지고 멀티모듈의 분리의 장점이 흐려진다.

코틀린 변성(Variance)

코틀린의 변성에 대해서 알아보자.

변성

코틀린에서 변성은 제네릭 타입의 상속 관계가 어떻게 적용되는지를 다루는 개념이다.

변성은 타입 파라미터가 달라질 때 제네릭 타입의 하위 타입 관계가 어떻게 달라지는지를 설명하는 제네릭 타입의 한 측면이다.

변성은 공변성, 반공변성, 불변성 등으로 나눌 수 있다.

변성은 주로 타입 파라미터의 상속 관계를 관리하는 데 사용되며, 이를 통해 부모 타입과 자식 타입 간의 변환 가능성을 제어한다.

왜 제네릭 타입은 타입 인자 사이의 하위 타입 관계를 그대로 유지하고 어떤 타입은 그렇지 못할까?

이러한 구분은 어떤 제네릭 타입이 자신의 타입 파라미터를 취급하는 방법에 달려 있다.

  • T 타입의 값을 반환하는 연산만 제공하고 T 타입의 값을 입력으로 받는 연산은 제공하지 않는 제네릭 타입인 생산자
  • T 타입의 값을 입력으로 받기만 하고 결코 T 타입의 값을 반환하지는 않는 제네릭 타입의 소비자
  • 위 두 가지 경우에 해당하지 않는 나머지 타입들

공변성

공변성은 제네릭 타입의 상속 관계가 같은 방향으로 적용되는 경우를 말한다.

부모 타입은 자식 타입을 대체할 수 있다는 것이다.

왜 불변 컬렉션 타입은 하위 타입 관계가 유지될까?

불변 컬렉션은 T 타입의 값을 만들어내기만 하고 절대 소비하지 않는다.

예를 들어, List 의 계약은 String을 돌려주는 것이다.

이런 경우 제네릭 타입이 타입 인자에 대해 공변적이라고 말한다.

코틀린에서 생산자 역할을 하는 타입은 모두 공변적이다.

주의! 불변성과 공변성은 같지 않다. 가변 타입을 공변적으로 만들 수도 있다.

코틀린에서 out 키워드를 사용해서 공변성을 나타낸다.

out은 해당 타입이 읽기 전용임을 의미한다.

반공변성

반공변성은 제네릭 타입의 타입 파라미터가 하위 타입에서 상위 타입으로 변환될 수 있음을 나타낸다.

T가 S의 상위 타입일 때, Consumer는 Consumer로 대체될 수 있다.

코틀린에서 in 키워드를 사용하여 반공변성을 나타낸다.

in은 해당 타입이 쓰기 전용임을 의미한다.

불변성

불변성은 제네릭 타입이 상속 관계에서 변하지 않는다는 의미다.

즉, 상위 타입을 하위 타입으로 변환할 수 없다는 뜻이다.

변성을 합리적으로 사용하면 타입 안전성을 해치지 않으면서 API 유연성을 향상시킬 수 있다.

CRLF와 LF

LF will replaced by CRLF

인텔리제이에 CheckStyle을 적용하여 코드 컨벤션을 적용하는 일이 있었다.

코드 스타일을 적용을 잘 해도 인텔리제이에서 주의 표시가 나와서 확인해보니, LF will replaced by CRLF라는 경고 메시지가 출력되어서 알아보았다.

CRLF(Carriage Return Line Feed)

먼저 CRLF에 대해서 알아보자.

CSLF는 캐리지 리턴과 라인 피드를 의미하는 조합이다.

텍스트 파일에서 줄 바꿈을 나타내는데 사용된다.

  • Carriage Return(CR) : \r로 표현되며, 커서를 현재 줄의 맨 앞으로 이동시키는 명령이다.
  • Line Feed(LF) : \n로 표현되며, 커서를 다음 줄로 이동시키는 명령이다.

이러한 두 제어 문자가 결합한 형태인 CRLF는 주로 Windows 시스템에서 사용된다.

텍스트 파일에서 줄바꿈을 나타내기 위해 \r\n를 사용한다.

CRLF는 초기 컴퓨터 시스템에서 텍스트를 출력할 대, 프린터가 종이를 반환하면서 새로운 줄로 이동할 필요가 있어 두 개의 문자가 사용되었다.

LF(Line Feed)

하나의 제어 문자로, 커서를 다음 줄로 이동시키는 역할을 한다. 표기는 \n

LF는 주로 Unix와 Linux 시스템, Mac에서 사용된다.

Unix 시스템이 등장하면서, 줄바꿈을 처리하는 방식으로 하나의 문자만 사용하게 되었다.

원인

결과적으로 OS 차이에 의한 Git 설정 이슈이다.

Windows 환경에서 Git을 사용할 때 자주 생기는 문제다.

Git은 기본적으로 Windows에서 CRLF를 사용하고 Unix/Linux/MacOS에서는 LF를 사용한다.

그래서 파일을 커밋하거나 푸시할 때 Git이 줄바꿈 문자를 자동으로 변환하려고 시도하며, 이로 인해 LF will replaced by CRLF 경고 메시지가 발생한다.

수정

이 문제를 해결하려면 Git의 줄바꿈 변환 설정을 조정해야 한다.

이러면 줄바꿈 문자가 일관되게 유지된다.

터미널을 열고 아래의 커맨드를 입력하자.

다른 방법으로는 .gitattributes 파일 사용하는 방법이 있다.

프로젝트 루트에 .gitattributes를 생성하고 * text=auto를 적어넣는다.

인텔리제이 하단부에 line seperator를 설정하는 방법도 있다.

abc