A에게 하는 답변: ~는 확인해 보셨나요? B에게 하는 답변: IDE 화면 캡쳐해 공유 후, 혹시 이것과 차이점이 있으실까요? C에게 하는 답변(이력이 없을 경우): 이력 찾아본 뒤 링크 공유. C에게 하는 답변(이력이 있을 경우): 이력은 따로 관리하지 않습니다. 혹시 ~는 확인해 보셨나요?
이력이 잘 관리되고 있는 팀에서는 C 사원의 답변이, 그렇지 않을 경우엔 혼자서 어느정도 원인을 파악해 확인해봐야 될 부분을 좁혀준 B 사원의 질문이 가장 마음에 들겠네요.
다시 주제로 돌아가서, 어떤 상황에서건 혼자서 해보고 질문하는 것은 매우 중요한 요소입니다. 이것은 질문하는 사람의 시간을 뺏는 것을 최소화하며 모르는 부분을 찾아 해결하는 능력을 향상시킬 수 있기 때문입니다.
'나 열심히 찾아봤는데 아직 잘 모르겠어... 이것까진 생각해봤는데 도와줘..'가 질문을 읽은 사람에게 느껴지는 질문이 가장 좋은 질문이라고 생각합니다.
30분은 찾아보자
그렇다고 3~4시간, 하루 이틀씩 하나의 문제를 가지고 끙끙 앓을 필요는 없습니다. 중요한 것은 질문하기 전 고민을 해 봤는가이지 시간적인 노력이 아니기 때문입니다.
따라서 일정 시간을 정하고, 그 시간이 넘어서도 잘 모르겠으면 질문을 해 문제를 해결하는 것을 권장드립니다.
제가 정한 규칙은 30분입니다. 2년간의 축척된 삽질을 바탕으로 통계를 내 본 결과, 30분 정도 찾아보면 내가 해결할 수 있는 문제인지 파악할 수 있는 최소 시간이었음을 깨달았기 때문이죠.
물론 이 시간은 온전히 제 경험을 통한 저만의 의견입니다. 하지만, 질문을 하기전 혼자 고민해보는 시간의 마지노선을 정하는 것을 추천드립니다.
2. 하기 전에 물어보자
혼자서 개발하실 경우 큰 문제는 없겠지만 팀원들과 함께 업무를 할 경우에는무조건 질문하시는 것을 추천드립니다.
개발자라는 직업은 문제 해결을 하는 직업이기 때문에 하나의 논제에도 다양한 해결 방안이 나오기 마련입니다. 코드 리뷰 문화가 잘 자리잡은 팀에서도 작은 개발 단위인 PR(Pull Request)에서조차 여러 의견이 나올 수 있습니다.
아래 예시를 봐볼까요?
A 사원: 파일 업/다운로드 기능 개선건 PR 올렸습니다. B 대리: 네 확인해 볼게요~ (확인 중) A 사원님. 파일 확장자나 파일 다운로드 제한 기능 같은게 없는 것 같은데 이유를 알 수 있을까요? 그리고 지금까진 사내 OBS를 써왔는데 트래픽이 많아져서 클라우드 연동해서 쓰기로 어제 결정났거든요. 이런 것들 고려해서 다시 PR 올려주세요~ (PR Close) A 사원: ... (타닥타닥)
오늘 결론냈던 내용이 고객 요청으로 인해 요구사항이 변경되었을 수 있고, 내가 고려하지 못한 부분까지 다른 팀원들이 생각할 수 있기 때문에 재작업을 하지 않기 위해선 코드를 구현하기 전 다시 한 번 구체적인 논의를 진행 후 작업을 진행하시는 것을 권장드립니다.
이런 논의를 자주 하다보면 함께 일하는 팀원의 개발 스타일이나 사고를 알 수 있어 점차 긍정적인 방향으로 업무 처리를 진행할 수 있습니다. 주니어 개발자라면 대부분이 본인보다 높은 경력을 가지고 있을 것이기 때문에 이러한 논의에서 배울 수 있는 점이 매우 많으니 작업 이전에 'B 대리님. 코드 구현 이전에 고려한 내용을 정리해 봤습니다. 이런 방향으로 구현할 계획인데 보완할 점이나 고려해야할 부분이 추가로 있을까요?'라고 한 번 물어보세요.
3. 신기술. 그걸 도입해서 얻는 이점이 뭔데?
MSA, AI, DDD, 헥사고널 아키텍쳐 등 다양한 개념들이 쓰나미처럼 몰려오고 있습니다. 개발 실력 향상을 추구하는 주니어 개발자에게 있어 이러한 기술들은 굉장히 해보고 싶고, 구미가 당길법한 것들입니다. 이러한 것들을 도입해보자! 제안해본 분들도 있을 것이라 생각됩니다.
어떤 답변을 받았었나요? 부정적이거나 흐지부지 끝난 경우 많았었나요? 혹은, 다음과 같은 질문을 받아보셨나요?
'이걸 도입하면 얻는 이점이 뭐죠?'
신기술 도입에는 사실 숨겨진 이면이 있습니다. 공수라고 하는 괴물이지요. 신기술을 공부하고 기존 시스템에 도입하는 그 긴 시간동안 팀은 별도의 성과를 내야 합니다. 왜냐면 회사니까요.
회사 입장에서는 신기술을 도입하건 1990년대 java 6을 도입하건 시스템이 개발되어 수익을 얻는 것에 더 관심을 많이 가질 것입니다.
들인 노력에 비해 기대 효과가 크지 않은 것들에 대해 시간과 노력을 투자하는 것은 옳지 않다고 생각하겠지요.
만약 도입하고 싶거나 해보고 싶은 것들이 있다면 팀에서 공수를 들여야 하는 합당한 이유들을 준비하거나 사이드 프로젝트로 공부하시는 것을 추천드립니다. 개발자이기 이전에 월급을 받는 회사원이라는 점 명심하세요.
4. 몰라도 자신있게 의견을 말하자. 고집은 부리지 말고..
사실 생각보다 팀에서 주니어에게 바라는 것은 크게 없습니다. 지금 당장 시스템을 개선한다던지 신규 프로젝트를 할거면 경력 있는 개발자를 채용하겠죠. 그럼 왜 신입, 주니어를 채용할까요?
제가 생각하기엔 두 가지가 있습니다. 그것은 바로 안정성을 흔드는 열정과 아직 열려있는 성장 가능성입니다.
팀에 열정을 불어넣는 사람이 된다면 지금 당장 팀에 도움이 되지 않아도 팀원으로서 팀에 이바지 할 수 있지 않을까요?
따라서 최대한 업무에 적극적으로 참여하고 자신있게 의견을 제안하고 말해봅시다. 모르는게 있으면 '제가 잘 몰라서 그러는데 이건 뭔가요?' 자신있게 궁금해 하세요.
몰라도 괜찮습니다. 잠깐 부끄러운 대신 그 지식은 부끄러운 만큼 오래 갈테니까요.
오늘은 개발적인 것보다는 마음가짐에 대해 이야기 해봤습니다. 사실 이 모든 이야기는 제가 신입때 경험했던 것들을 바탕으로 글을 작성한 것이니 저보다 뛰어나신 분들은 '뭐 당연한 소리를 글로 쓰고 있어' 라는 생각을 하실 수도 있습니다.
근데... 팀원들은 본인에 대해 어떻게 생각할지 모르잖아요? 항상 겸손해야 합니다
2부에서는 지금 쓴 내용을 토대로 2년 반동안 어떻게 성장하고 있는지 조금 더 구체적으로 작성해 보겠습니다. 아직 햇병아리지만 귀엽게 봐주셨으면 감사하겠습니다.
Junit4에서 사용되는 @Ignore가 Junit5에서는 @Disabled로 변경되었습니다.
이 뿐만 아니라, @DisabledIf, @EnabledIf, @EnabledOnOs 등 다양한 조건문 어노테이션을 통해 테스트를 조건부로 실행시킬 수 있습니다.
예시
@Disabled("서버마다 공통된 mount 정보가 없어 테스트 불가능.")
@Test
public void test_get_mountDetailInfo_FAILED_when_data_not_exist() throws Exception {
// Given
MountStatus mountStatus = new MountStatus(getMountPath());
// When
String result = mountStatus.getDetail();
// Then
assertEquals("Nas path : devfs\n" + "Mount local path : /dev/\n" +
"Total : 189\n" + "Used : 189\n" + "Usage percent : 100%\n", result);
}
기존 junit4에서는 @FixedMethodOrder와 MethodSorters의 조합으로 테스트 순서를 정의할 수 있었습니다. 하지만 해당 어노테이션으로는 테스트 메소드 순서 정의에 있어 기능이 적어 일부 제한이 존재하였습니다. (MethodSorters.DEFAULT, MethodSorters.JVM, MethodSorters.NAME_ASCENDING 세 개가 전부)
Junit5에서는 @TestMethodOrder 및 TestClassOrder를 지원하며 @Order를 통해 원하는 테스트 클래스/메소드 실행 순서를 정의하여 실행이 가능해집니다.
기존 junit4에서는 inner class를 이용한 테스트 그룹핑이 불가능하였으나 junit5부터는 @Nested 어노테이션을 이용할 시 테스트간 그룹핑이 가능해졌습니다.
하나의 테스트 클래스에서 테스트를 수행하지만 수행하는 테스트의 기능이 여러가지일 경우 기능별로 묶어 수행 결과에 대해 좀 더 직관적으로 확인이 가능해집니다.
적용 대상 클래스를 확인해보니 3가지 그룹으로 그룹핑이 가능했고, 이들을 @Nested로 그룹화하여 inner class로 코드를 리팩토링하였습니다.
기존 결과(변경 전)
변경 후 결과
다음과 같이 변경된 이후 좋았던 것은 그룹핑 전엔 보이지 않았던 기능별 테스트 목록이 그룹핑 후 조금 더 직관적으로 보였다는 부분입니다. @DisplayName과 함께 사용하니 그룹별로 어떤 기능의 테스트가 미흡한지 보다 더 잘 알 수 있어 테스트 코드를 추가로 보완할 수 있었습니다.
이제 NotificationServiceStatusTest를 통해 해당 테스트의 예시를 들어보겠습니다. 해당 코드 또는 아래 내용을 참고해주시면 감사하겠습니다.
기존 코드 (변경 전)
public class NotificationServiceStatusTest {
@Test
public void test_collector_status_false() {
assertFalse(new NotificationServiceStatus(WRONG_DOMAIN, NotificationService.COLLECTOR).check());
}
@Test
public void test_file_status_true() {
assertTrue(new NotificationServiceStatus(conditionConfiguration.getStatsApiUrl(), NotificationService.FILE).check());
}
@Test
public void test_file_status_false() {
assertFalse(new NotificationServiceStatus(WRONG_DOMAIN, NotificationService.FILE).check());
}
@Test
public void test_webHook_status_true() {
assertTrue(new NotificationServiceStatus(conditionConfiguration.getStatsApiUrl(), NotificationService.WEBHOOK).check());
}
@Test
public void test_webHook_status_false() {
assertFalse(new NotificationServiceStatus(WRONG_DOMAIN, NotificationService.WEBHOOK).check());
}
@Test
public void test_tag_status_true() {
assertTrue(new NotificationServiceStatus(conditionConfiguration.getStatsApiUrl(), NotificationService.TAG).check());
}
@Test
public void test_tag_status_false() {
assertFalse(new NotificationServiceStatus(WRONG_DOMAIN, NotificationService.TAG).check());
}
}
실행 결과 (변경 전)
변경 후
해당 코드는 모두 NotificationService enum 클래스의 값으로 테스트를 수행하므로 @EnumSource를 이용한 테스트로 통합하였습니다.
public class NotificationServiceStatusTest{
@ParameterizedTest
@EnumSource(NotificationService.class)
@DisplayName("NotificationServiceStatus Status check using NotificationService - SUCCESS")
public void test_notificationServiceStatus_true(NotificationService notificationService) {
assertTrue(new NotificationServiceStatus(conditionConfiguration.getStatsApiUrl(), notificationService).check());
}
@ParameterizedTest
@EnumSource(NotificationService.class)
@DisplayName("NotificationServiceStatus Status check using NotificationService and Wrong Domain - FAIL")
public void test_notificationServiceStatus_false(NotificationService notificationService) {
assertFalse(new NotificationServiceStatus(WRONG_DOMAIN, notificationService).check());
}
}
inner class내에서 @BeforeAll을 적용할 경우 정상적으로 테스트코드가 적용되지 않는 오류가 발생했습니다. 오류 내용은 다음과 같습니다.
org.junit.platform.commons.JUnitException: @BeforeAll method
'public void com.test.nhn.condition.master.ConditionTest.beforeClass() throws java.io.IOException'
must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
오류의 마지막 문구에 @TestInstance(Lifecycle.PER_CLASS) 적용시 static을 적용하지 않아도 된다는 문구가 존재하여 @TestInstance(Lifecycle.PER_CLASS)를 사용해 테스트 코드를 작성하였습니다.
결론
현재 적용한 기능 말고도 여러 기능이 있지만 예시로 든 프로젝트에서는 테스트 코드에 큰 복잡성이 없어서 모든 기능을 다 사용해보진 못한 것 같습니다.
테스트 코드에 junit5를 적용하면서 느낀 것은 다음과 같습니다.
@DisplayName을 통해 해당 테스트가 무엇을 수행하는지 명시적으로 작성할 수 있습니다.
예제에는 맨 마지막에 SUCCESS, FAIL을 붙였는데 접두사로 사용하면 좀 더 괜찮지 않을까.. 싶습니다.
JunitParams(@ValueSource 등)을 통해 어떤 값을 테스트 인자로 사용하는지를 직관적으로 알 수 있습니다.
@Nested를 통해 테스트 그룹핑이 용이해졌습니다.
@DisabledIf와 같은 조건문을 통해 환경별/상황별 테스트 실행 유무를 관리할 수 있습니다.
함수형 프로그래밍을 위해 고안된 내결함성(fault tolerance) 라이브러리입니다. Resilience4j는 Circuit Breaker, Rate Limiter, Retry 또는 Bulkhead 등 서비스 장애 대응을 위한 다양한 기능을 제공하고 있습니다. resilience4j는 java8을 통해 기능 개발 됐으며 함수 인터페이스, 람다 식과 같은 고차 함수를 제공합니다.
지원 버전
1.x: Java 8 이상 2.x: Java 17 이상
핵심 모듈
resilience4j-circuitbreaker: 회로 차단 (Circuit breaking)
resilience4j-ratelimiter: 속도 제한 (Rate limiting)
resilience4j-bulkhead: 격벽 (Bulkheading)
resilience4j-retry: 자동 재시도(동기화 및 비동기화) (Automatic retrying (sync and async))
resilience4j-timelimiter: 시간 초과 처리 (Timeout handling)
resilience4j-cache: 결과 캐싱 (Result caching)
핵심 모듈 중 circuit breaker, ratelimiter, bulkhead에 대한 설명만 작성하겠습니다. 각 라이브러리에 대한 추가 설명이 궁금하신 분들은 resilence4j github을 참고해주시기 바랍니다.
resilience4j-circuitbreaker
Circuit Breaker란?
최근의 Web 및 App의 백엔드 서버 시스템은 여러 개의 서비스가 API나 RPC로 연결된 네트워크로 구성되어 있습니다. 만약 이 네트워크 중 하나가 갑자기 전혀 응답하지 않게 되는 상황이 발생하면 어떻게 될까요? 동작하지 않는 서비스 접속 시 타임아웃될 때까지 차단되어, 의존성이 있는 서비스까지 연쇄적으로 멈출 가능성이 있습니다. 만약 네트워크 전체를 파악하고 있는 사람이 아무도 없다면, 근본적인 원인이 어느 서비스에 있는지를 알아내기까지 시간이 걸리게 됩니다.
Circuit Breaker란, 원격 접속의 성공/실패를 카운트하여 에러율(failure rate)이 임계치를 넘어섰을 때 자동적으로 접속을 차단하는 시스템입니다. Circuit Breaker는 상태 머신(State Machine)으로 나타낼 수 있습니다. 접속 성공과 실패 이벤트가 발생할 때마다 내부 상태를 업데이트하여 자동적으로 장애를 검출하고 복구 여부를 판단합니다.
에러율이 임계치를 넘어서면 OPEN 상태가 됩니다. 모든 접속은 차단(fail fast)됩니다.
HALF_OPEN
OPEN 후 일정 시간이 지나면 HALF_OPEN 상태가 됩니다. 접속을 시도하여 성공하면 CLOSED, 실패하면 OPEN으로 되돌아갑니다.
resilience4j-ratelimiter
Rate Limit란?
서버는 제공할 수 있는 자원이 한정되어 있기 떄문에 특정 임계치까지만 클라이언트의 요청을 허용하는 정책을 의미합니다. 제한치를 넘어선 요청에 대한 요청을 거부하거나 Queue로 만들어 실행 기능 제공합니다.
https://etloveguitar.tistory.com/126
resilience4j-bulkhead
Bulkhead란?
동시 요청 수를 제한하는 기법입니다. 요청 수에 도달한 이후 요청에 대해서는 예외처리를 진행합니다.
Resilence4j bulkhead pattern
SemaphoreBulkhead
동시 요청 수를 제한을 두고 요청 수에 도달한 이후 요청에 대해서는 BulkheadFullException이 발생합니다.
FixedThreadPoolBulkhead
시스템 자원과 별도로 thread pool을 설정하고 설정된 thread pool은 서비스를 제공하기 위한 용도로만 사용합니다. 그리고 thread pool과 별도로 waiting queue를 설정할 수 있습니다. 만약 thread pool과 waiting queue 가 full 인 경우 BulkheadFullException이 발생합니다.
resilience4j-timelimiter
TimeLimiter는 future supplier의 time limit을 정하는 API입니다.
결론
Resilience4j는 서비스 장애 대응을 위해 만들어진 오픈 소스 라이브러리입니다. 해당 라이브러리에서는 다양한 기능을 제공합니다.
resilience4j-circuitbreaker: 회로 차단(Circuit breaking)
resilience4j-ratelimiter: 속도 제한(Rate limiting)
resilience4j-bulkhead: 격벽(Bulkheading)
resilience4j-retry: 자동 재시도(동기화 및 비동기화)(Automatic retrying (sync and async))
resilience4j-timelimiter: 시간 초과 처리(Timeout handling)
해당 본문이 테스트 코드에 대한 정리가 잘 되어 있어서 해당 글을 정독하시는걸 추천드립니다.
테스트 코드란?
소프트웨어의 기능과 동작을 테스트하는데 사용되는 코드.
개발자가 작성한 코드를 실행하고 예상된 결과가 나오는지 확인하는데 사용
테스트 코드의 종류로는 단위 테스트. 통합 테스트, 시스템 테스트, 사용자 인수 테스트 등이 존재
개발자가 주로 다루는 테스트 코드
단위 테스트 (Unit Test): 개별적인 코드 단위(ex) 함수,메서드)가 의도한대로 작동하는지 확인하는 과정
통합 테스트(Integration Test): 서로 다른 모듈들 간의 상호작용을 테스트하는 과정.
테스트 코드를 작성하는 이유
테스트 코드를 작성하면 요구사항의 기능적인 항목들을 정리하는 경험을 가질 수 있다.
테스트 코드를 통해 기능 수정간 발생할 수 있는 에러 상황에서 보호받을 수 있다.
테스트 코드를 작성하면서 복잡한 의존성에 대한 고민을 하게 되고, 더 나은 코드를 작성할 수 있게 된다.
내가 생각하는 테스트 코드란 ?
테스트 케이스 작성간 발생할 수 있는 단점은 테스트 코드에 대한 이해도가 부족한 개발자가 작성한 테스트 코드 뿐이라고 생각합니다. 올바른 테스트 코드 작성은 서비스의 코드 품질을 높이며 유지보수에 큰 도움을 줄 수 있습니다. 그렇기에 테스트 코드의 올바른 이해가 작성 이전에 진행되어야 된다고 생각하고 저 또한 더 좋은 테스트 코드 작성을 위해 많은 공부를 하고 있습니다.
TDD(Test Driven Development) 방식도 존재하지만 이러한 방식에 대해서는 주니어 개발자의 입장에서 다소 회의적입니다. '숙련된 개발자일수록 TDD가 용이해 질 것이라 생각하지만 개발 순서의 차이일 뿐 동일한 결과물이 나오지 않을까...?' 또는 '오히려 시간이 더 소요되지 않을까..?' 하는 생각도 들고요.
주니어 개발자들은 개발을 진행하고 TC를 작성하며 더 좋은 코드를 향해 리팩토링하는 과정을 거치는 것이 보다 더 좋은 코드를 작성할 수 있는 방법이 아닐까 싶습니다. 어느 정도 코드 작성에 숙련되었다 생각된다면 TDD에 대한 고려도 필요하겠지만요.
위의 의견을 요약하면 내가 만드는 기능이 어떤 요구사항이 존재하는지, 어떤 케이스가 예외 사항이 될 수 있을지 정리하고 테스트 코드를 더 간결하게 짜기 위해 서비스 로직 구성을 고려해야 합니다. 그러다 보면 어느새 좋은 코드(clean code)를 작성하는 개발자가 되어 있을 것이라고 생각한다는 의견을 남기며 글을 마무리하겠습니다.