프로그래밍 일기 — 칠전팔기 기록
계속 넘어지고, 다시 계속 일어나는 것이 프로그래머의 숙명
어렸을 때 나는 레슬링 팬이었다. 존 시나(John Cena)는 그중에서도 내가 가장 좋아하는 스타였다. 특히 그의 캐치프레이즈였던 “Never Give Up”은 내게 많은 용기를 주었다. 그는 하루도 훈련을 거르거나 늦는 법이 없었고, 가장 먼저 시작해서 가장 나중에까지 링에 남아있었다고한다. 그의 삶은 레슬러로써 그의 꿈을 이루기 위한 끝없는 노력으로 보답했고, 그는 지금 이 분야를 대표하는 최고의 스타가 되었다.
어느 분야나 성공하기 위해서는 마찬가지겠지만, 특히 프로그래밍 분야에서는 더 그런것 같다. 프로그래밍을 하면서 수많은 난관에 빠진다. 도대체 왜 이런 에러가 뜨는지 모르겠고, 도대체 무엇이 잘 못되어 있는지 알아내기 어려울 때가 많다. 그럴 때마다 수 시간의 트러블슈팅, 소위 “삽질"을 통해 하나하나 해결해 나간다. 나중에 문제를 해결했을 때는 너무나 어처구니없는 실수처럼 느껴진다. 하지만, 그 경험을 통해 하나씩 배워나가면서 다음에는 똑같은 실수를 반복하지 않기위해 노력한다. 프로그래밍은 칠전팔기를 배울 수 있는 가장 좋은 방법이다. 그리고 이 과정에서 끝까지 포기하지 않는 자들이 살아남는다. “강한자가 살아남는게 아니라, 살아남는자가 강한 것이다.”라는 말은 프로그래밍에서는 다른 분야보다도 더 진리로 통용된다고 생각한다.
그 과정에서 가장 중요한 것 중 하나가 실수를 통해 배워나간 경험들을 기록하는 것이다. 나 역시 그러한 노력을 게을리 할 수 없다. 이번 장에서는 프로젝트를 진행하면서 겪었던 어려움과, 그 어려움을 어떻게 해결했는지에 대해 기록으로 남기려고한다. “내 실수의 역사"를 기록하면 다시 똑같은 실수를 반복하지 않을 것이라고 믿으면서.
내 실수의 역사
사건1 — AWS S3 사진 링크 접속 불량
백엔드에서 S3로 사진을 올릴 수 있는 기능을 개발했다. 이 기능은 올린 사진에 대한 URL을 리턴하도록했다. 분명 URL은 잘 리턴하는데 계속해서 브라우저에서 해당 URL을 열면 사진이 열리지 않았다.
정말 이해가 가지 않았다. 일단 오타가 있을 수 있다고하니 S3에서 직접 사진을 열어보기로했다. 그리고 여기에서의 URL주소와 내 메서드가 리턴한 URL 주소를 비교해 보기로했다.
둘을 비교해보니 답을 알 수 있었다. S3상에서의 사진 URL은 사실 .s3.{지역명(region name)}.amazonaws.com 까지 포함해야하는 것이었다. 이 부분이 내 메서드 URL에서 빠졌기 때문에 브라우저상에서 열수가 없었던 것이다. 이런 어처구니 없는 실수라니!
- 메서드가 리턴한 URL: https://elasticbeanstalk-ap-northeast-2-714316684288/profileImg/turtle.png
- S3상에서의 사진 URL: https://elasticbeanstalk-ap-northeast-2-714316684288.s3.ap-northeast-2.amazonaws.com/pictures/%E1%84%80%E1%85%A5%E1%84%87%E1%85%AE%E1%86%A8%E1%84%8B%E1%85%B5.png
그래서 다시금 리턴하는 URL이 이부분을 포함하도록 코드를 수정하고 프론트 엔지니어분들께 테스트를 부탁했더니 이제는 정상으로 뜬다.
사건 2 — 갑자기 자취를 감춰버린 라이브러리들
사진을 올리는 기능에 대해 테스트코드를 작성하려할 때였다. 왠지 모르겠는데 어느 순간 갑자기 라이브러리들이 사라진 것을 발견했다. 분명, 해당 라이브러리들은 build.gradle
파일에 정의가 되어 있었는데, 도대체 왜 사라진거지?
이 문제 때문에 깜짝놀라서 몇 번이고 프로젝트를 다시 생성했지만, 문제가 사라지지 않았다. 그래서 팀원분들께 도움을 요청하고 설치된 자바 버전도 확인하고, 아예 처음부터 프로젝트를 SpringInitializer
로 처음부터 생성해서 본 프로젝트에서 패키지를 하나씩 가져와 해당 패키지에서 사용되는 라이브러리들을 하나씩 인식되는지 보려했다. 그렇게해도 관련 라이브러리가 build.gradle
에 정의되어 있는데도 불구하고 인식이 되지 않았다.
그래서 IntelliJ를 삭제하고 다시 설치해보기까지했다. 그럼에도 불구하고 문제는 사라지지 않았다. 이 문제가 해결되지 못하면 프로젝트를 진행할 수 없어 발을 동동 굴렀는데, IntelliJ에서 관련 문제에 대한 조금이라도 관련이 있는 설정을 찾아볼 수 있을까하는 간절한 마음에 화면 상단의 메뉴들을 하나씩 훓어보았다. 그러던 중 File 메뉴 아래 Repair IDE라는 것을 확인할 수 있었다. 호기심이 들어 이것을 클릭해 보았다.
화면에 위와같이 대화 상자가 하나 나타났는데, 하단에 Everything Works Now와 Rescan Project Indexes라는 메뉴를 확인할 수 있었다. 당연히 Everything Works Now는 “모든 것이 잘 된다.”는 의미니 내 상황에 맞는 표현은 아닐 것이다. 그래서 Rescan Project Indexes를 눌러보았다.
그러더니 프로젝트에 대한 스캐닝이 이루어지면서, 완료 후에는 보이지 않던 라이브러리들이 보이기 시작했다. 집 나간 아이들이 돌아온 것이다!
그럼에도 불구하고 아직 돌아오지 않은 아이들이 몇 명 더 있었다. 그래서 다시 한번 더 Repair IDE를 클릭해 프로젝트에 대한 스캐닝을 했다. 그렇게 3번 정도 위 과정을 반복하니, 비로소 모든 라이브러리들이 집에 돌아온 것을 확인할 수 있었다. Yeah!
이제 부터 갑작스럽게 라이브러리들이 집에 나가면 IDE라는 집을 고쳐야 아이들이 돌아온다는 것을 배웠다. 이렇게 쉬운데 왜 이제 발견했을까? 어쩌면 이 우연한 발견의 순간을 만나기 위해 그렇게 많은 삽질을 한 것이 아닐까싶다. 내가 이제까지 만난 그 어떤 새로운 에러를 해결할 때와 마찬가지로 말이다. 신이 내 간절함에 반응해주신 거라 생각하고 감사하며 앞으로도 열심히 해야겠다고 다짐했다.
사건 3 — “한번에 1MB이상의 요청은 안받습니다.”(2)
AWS S3를 활용해 사진 업로드를 하는 기능을 구현하다보니 사진이 1MB이상 올라갔을 때 용량 초과 에러를 띄우는 것을 확인했다. 핵심 메시지는 아래와 같다.
// S3에 사진 업로드시 1MB를 초과하면 아래와 같은 에러가 나온다.
Caused by: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field package exceeds its maximum permitted size of 1048576 bytes.
즉, 1MB이상의 파일은 업로드가 불가능하다는 것인데, 이렇게 파일 제한 사이즈가 작으면 사진을 올리는데 있어 상당한 제약이 있을 것으로 판단되었다. 따라서 최소 이에 10배인 10MB를 설정할 필요가 있었다.
다수의 자료를 참조했는데, 아래와 같이 application.properties
상에서 파일 업로드 허용 사이즈를 넓히라는 조언을 얻었다. 즉, Spring내 Servlet에서 파일 업로드를 담당하는 Multipart
클래스에 접속하여 최대 파일 요청 사이즈를 확대시키는 것이다.
# application.properties내 파일 업로드 최대 허용 사이즈를 정의
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.max-file-size=10MB
그러나 위 설정을 해 주었는데도 불구하고, 여전히 같은 예외가 나왔다. 그래서 조금 더 뒤져보니 StackOverFlow에서 귀한 답을 얻을 수 있었다. 아래와 같이 @Bean
을 하나 등록하는 것이었다. 즉, MultipartConfigElement
클래스를 활용해 파일 업로드 요청 당 최대 사이즈를 수정하는 설정을 정의하는 것이다. 이렇게하면 Spring은 Multipart
클래스를 활용해 파일을 업로드 할 때마다 하기 클래스를 주입해 활용할 수 있다.
// 프로젝트 메인 클래스 파일내 아래 Bean을 등록한다.
// MultipartConfigElement 클래스를 활용해 파일 업로드 요청 당 최대 사이즈를 수정한다.
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize("10MB");
factory.setMaxRequestSize("10MB");
return factory.createMultipartConfig();
}
위 방법을 통해서 1MB이상 10MB이내 사진을 업로드하는데 성공했다. 사진 업로드 요청 당 최대 허용 파일 사이즈는 10MB보다 더 큰 단위를 허용할 수도 있지만, 그만큼 많은 데이터를 불러와야하기 때문에 브라우저에서 출력하는데 있어 느려질 수 있다. 따라서 이 허용 범위는 개발하고자하는 어플리케이션의 요구사항 및 환경에 따라 다를 수 있다.
사건 4 — Docker상에서 S3테스트가 잘 안되네…
S3 사진 업로드 기능에 대한 테스트 코드를 작성하려고했는데 생각만큼 쉽지 않았다. 실제 S3를 사용하면 테스트 코드로 인해 잘못된 데이터가 입력될 수 있으니, 이 상황을 피하기 위한 방법을 모색했고, 2가지 방법을 찾아냈다.
S3Mock
을 활용해 S3를 모사하는 방식- LocalStack + TestContainer + Docker를 활용해 가상 환경에서 S3를 모사한(비슷하게 동작하도록 만들어진 객체)를 올려 거기서 데이터의 업로드를 테스트하는 방식
1번 방식 — 결과: 실패
1번이 상대적으로 단순한 방식이어서 해당 방식을 사용하려했으나, S3Mock
클래스 자체는 완전한 S3처럼 비슷하게 동작하게 만들기 힘들었다. 파일 업로드를 테스트하려는 과정에서 자꾸 실제 S3를 활용하려고하는 모습이 보였다. 내가 제대로 사용하지 못해서일 수도 있지만 어쨌든, S3Mock로는 S3를 모사하는 테스트를 하기 어려웠고, 많이 찾아보았지만 어느 부분을 손대야할지 감이 잡히지 않았다. 너무 많은 시간을 지체할 수 없어 2번으로 가기로했다.
2번 방식 — 결과: 성공(3)
2번 방식을 조금 더 자세히 설명하자면 다음과 같다. Docker를 외부 설정 없이 Java 언어만으로 구축할 수 있는 TestContainers라는 오픈소스 라이브러리와 로컬의 단일 컨테이너에서 실행될 수 있는 AWS클라우드 서비스 에뮬레이터인 LocalStack으로 Docker상에서 컨테이너를 생성하고 거기에 AWS S3를 에뮬레이션하는 이미지를 올려 가상환경에서 S3를 테스트하는 방식이다.
TestContainers는 외부에서 따로 DB를 설정하거나 프로그램/스크립트의 실행이 필요없다는 장점이 있다. 컨테이너 기술을 사용하기 때문에 라이브러리를 설치하는 것보다 더 가볍게 테스트 환경을 구축하게 해준다. LocalStack은 로컬에서 단독으로 실행이 되므로, AWS 클라우드 서비스를 사용하는 웹 어플리케이션을 쉽게 테스트할 수 있게 해준다. 일부 유로 기능이 있지만 무료로 지원하는 기능이 상당 수 이다. 내가 활용하고자 하는 목적에도 유료 기능이 필요없기 때문에 적합한 방식이라고 판단되었다.
내가 활용한 방식은 LocalStack 컨테이너를 TestContainers로 실행하는 방식이다. TestContainers내에서 Localstack모듈을 지원하기 때문이다. 마찬가지로 AWS가 제공하는 다른 서비스인 redis, mysql등도 TestContainers로 구축이 가능하다. 이렇듯 AWS 서비스들을 모사할 수 있는 LocalStack 컨테이너를 TestContainers에서 바로 지원하기 때문에 매우 효율적인 방식이라 판단했다.
일단 아래의 의존성이 필요했다.
// localstack
testImplementation "org.testcontainers:localstack:1.16.3"
// testContainers
testImplementation "org.testcontainers:junit-jupiter:1.16.3"
// aws s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
이어서 아래와 같은 코드를 사용해 테스트를 진행했다. LocalStack + TestContainers로 Docker상에서 S3 에뮬레이션을 할 수 있게하는 이미지를 통해 S3기능을 테스트한다.
@Testcontainers
public class LocalStackTestContainersTest {
// LocalStack 이미지 이름 정의하여 이미지 생성
private static final DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack");
// localstack이미지를 통해 이미지 생성
@Container
LocalStackContainer localStackContainer = new LocalStackContainer(LOCALSTACK_IMAGE)
.withServices(S3);
// localstack이미지를 활용해 S3를 에뮬레이션하고, S3의 기능을 테스트
@Test
void test(){
AmazonS3 amazonS3 = AmazonS3ClientBuilder
.standard()
.withEndpointConfiguration(localStackContainer.getEndpointConfiguration(S3))
.withCredentials(localStackContainer.getDefaultCredentialsProvider())
.build();
// 1. S3 버킷 가명 설정
String bucketName = "foo";
// 2. 버킷 이름으로 버킷 생성
amazonS3.createBucket(bucketName);
System.out.println(bucketName +" 버킷 생성");
// 3. 이미지 업로드시 활용할 키값(파일 이름) 정의
String key = "foo-key";
// 4. 이미지 업로드시 활용할 컨텐츠값(파일 내용) 정의
String content = "foo-content";
// 5. 지정한 버킷에 파일 이름과 컨텐츠를 업로드
amazonS3.putObject(bucketName, key, content);
System.out.println("파일을 업로드하였습니다. key=" + key +", content=" + content);
// 6. 지정한 버킷에서 업로드한 파일을 지정한 키값으로 가져와 해당 파일이 정상적으로 업로드되었는지 확인
S3Object object = amazonS3.getObject(bucketName, key);
System.out.println("파일을 가져왔습니다. = " + object.getKey());
}
}
위 코드를 실행하고 시행착오를 거치면서 몇 가지 알게 된 점이 있다.
- DockerHub이라는 곳에서 남들이 올려놓은 이미지를 다운로드 할 수 있다. 남들이 코드를 올려놓을 수 있는 GitHub과 같은 개념이라 볼 수 있다. 나의 경우 LocalStack이라는 AWS S3를 모사할 수 있는 이미지를 원했기 때문에, 이를 DockerHub에서 CLI에 다음 커맨드를 입력해 다운 받았다(
docker pull localstack/localstack
). - Docker이미지 이름을 변수로 설정시 해당 이미지의 이름을 정확히 알아야한다. 나의 경우 이미지 이름이
“localstack/localstack”
이 아닌“localstack/localstack:latest”
였다. (private static final DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse(“localstack/localstack:latest”);0
) - Docker의 연결방법에는 여러가지가 있으나, 나의 경우 Docker for Mac을 활용했다. 로컬상에서 Docker를 설치하고, 해당 Docker에 IntelliJ에서 Services 창에서 연결해 주어야한다. Docker Desktop은 해당 사이트에서 설치 가능하다: https://docs.docker.com/desktop/install/mac-install/
위 처럼 도커를 연결해주지 않으면 아래와 같은 에러 메시지를 볼 수 있을 것이다.
// 시스템이 도커를 찾을 수 없을 때 아래 에러가 발생할 수 있다.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
여전히 도커를 인식 못한다면 아래 처럼 chmod
명령을 통해 터미널에서 도커에 접근하기 위한 권한을 열어준다. 접근 권한 문제일 수 있기 때문이다. 나의 경우에는 아래 과정은 필요없었다. ( chmod 666
은 “모든 유저에게 읽기/쓰기 권한을 허용한다는 의미다.”)
// chmod 명령어로 Docker에 접근 권한 설정
chmod 666 /var/run/docker.sock
Docker테스트가 성공하면 아래와 같은 메시지를 볼 수 있다.
사건 5 — POSTMAN에 보이지않는 문자?
어느 때처럼 코드를 수정하고 로컬에서 작성한 기능을 테스트할 때였다. 로그인이 안되었다. URL을 확인해 보니 문제는 없었고, Authorization은 로그인할 때 생성되는 JWT토큰이기 때문에 별도로 입력하지는 않았다. 그런데 도대체 뭐가 문제지? 에러 메시지는 아래와 같았다.
위 메시지를 그대로 복사해서 구글링 해본 결과, URL을 복사해 브라우저에 넣어보라는 말을 들었다. 실제 이전 URL과 같다고해도, 만약 다른 HTTP 요청에서 URL을 복사해 왔다면 숨겨진 문자가 있을 수 있다는 것이다. 본래 URL은 다음과 같았다: http://localhost:8080/api/users/login
해당 POSTMAN상의 URL을 아래와 같이 브라우저에 넣었더니 놀라운 일이 벌어졌다.
가만 기억을 더듬어 보니, 로그인 URL을 생성할 때 공통적으로 사용되는 URL을 복사해왔다. 예를 들어 회원가입은 http://localhost:8080/api/users/signup
이었다. 그래서 /signup
이전 부분만 복사해서 붙여넣기하여 맨 뒤 접미사만 /login
으로 바꿔주고 POSTMAN요청을 새로 생성했는데, 그렇게 했을 때 원치않는 문자가 삽입되었다는 것이다. 앞으로는 되도록 URL을 복사하지않고, 직접 키보드로 입력하는 것이 좋겠다는 생각이들었다. 생각해보면 조금 번거롭지만, 정확한 일처리를 위해 필요하다면 어쩔 수 없다. 그런데, 조금 더 생각해보니, 아래처럼 기존의 요청을 복사하고, 기존 요청의 URL을 바탕으로 수정하여 새로운 POSTMAN요청을 생성할 수 있다. 왠지 후자의 방법이 조금 더 효율적일 것 같다는 생각이 들었다.
참조:
(1) https://staticg.sportskeeda.com/editor/2017/09/57f29-1505828119-800.jpg
(3) https://loosie.tistory.com/817
(4) https://stackoverflow.com/questions/63037406/postman-error-parse-error-invalid-header-value-char