아래는 K6의 공식 문서 와 23년 2월 Tech 세미나 - 성능 테스트와 K6 도구 소개 영상에서 본 내용을 기반으로 정리한 글입니다.
K6
도구를 공부하게 된 계기는 접근성과 실용성 때문이다. IT 커뮤니티 동아리인 SIPE에서 스프링 퍼포먼스 트랙 이라는 주제로 나를 포함한 7명이 모였고, 여러 성능 테스트 도구 중에서 다수결로 K6가 선정되었다.
나는 현재 회사에서 nGrinder
를 사용해 성능 테스트를 수행하고 있다.
하지만 nGrinder
는 Jython/Groovy 기반의 스크립트 작성을 요구해, 이러한 언어에 익숙하지 않은 사람들에게는 복잡하게 느껴질 수 있다.
반면 K6
는 간결하고 직관적인 JavaScript 기반 스크립트 방식을 제공해 누구나 쉽게 접근할 수 있다.
또한, K6
는 사용자 친화적인 인터페이스와 효율적인 CLI 기반 환경 덕분에 새로운 사용자도 빠르게 적응할 수 있다.
Prometheus
같은 모니터링 도구와의 연동이 잘 되어 있어 성능 테스트와 모니터링을 통합적으로 수행할 수 있다는 점도 큰 장점이다.
K6 공식 문서가 잘 정리되어 있고, Github 기준으로도 다른 성능 테스트 도구보다 높은 star 수(26k)를 기록해 레퍼런스 자료가 풍부하다는 점도 마음에 들었다.
(참고로, nGrinder
는 2k star에 머물러 있다)
이러한 이유들로 K6
를 선정하게 되었고, 개념을 간단히라도 정리하기 위해 이 글을 작성하게 되었다.
장점
Grafana에서 만든 오픈소스 부하생성 도구
쉬운 테스트 수행, 개발자 중심의 성능 테스트 가능
CLI 툴을 이용하여 개발자 친화적인 API 제공
명령 줄 인터페이스(CLI, Command line interface) 또는 명령어 인터페이스는 텍스트 터미널을 통해 사용자와 컴퓨터가 상호 작용하는 방식을 뜻한다. 즉, 작업 명령은 사용자가 컴퓨터 키보드 등을 통해 문자열의 형태로 입력하며, 컴퓨터로부터의 출력 역시 문자열의 형태로 주어진다. ( - 위키백과 - )
성능
이라는 개념을 공부하기 전에, 왜 이 개념과 관련 도구들이 필요하게 되었는지 궁금했다.
나는 아직 사용자 1000만 명 이상의 대규모 서비스를 개발한 경험이 없지만, 지금까지의 경험을 바탕으로 생각해보면 다음과 같다.
서비스 사용자가 증가할수록 트래픽 양도 함께 늘어나며, 서버가 이를 감당할 수 있는지 성능 테스트를 통해 확인하고 그에 맞게 대응하고 보완해야 한다.
성능 테스트는 다양한 상황에서 시스템의 안정성과 효율성을 확인하는 데 중요한 역할을 한다. 내가 생각하는 성능 테스트의 활용 사례는 다음과 같다:
서비스를 출시하기 전, 예상되는 트래픽의 양을 서버가 감당할 수 있는지 성능 테스트로 파악한다. 일반적으로 예상 사용자 수의 3배로 부하 테스트를 수행해 예기치 않은 트래픽 급증에도 대비할 수 있도록 한다.
기존 서비스를 운영하면서 선착순 이벤트 등으로 갑자기 트래픽이 몰릴 때 발생하는 병목 지점을 성능 테스트(예: 스파이크 테스트)를 통해 식별한다.
멀티스레드 환경에서 비즈니스 로직을 비동기 방식으로 처리할 때, 논블로킹 방식 등을 고려하여 성능을 개선하기 위해 응답 속도와 TPS(초당 트랜잭션 수)를 성능 테스트로 측정한다.
아래는 최근 내가 공부한 성능 개념과 주요 병목 지점, 그리고 성능 테스트에 대한 내용을 정리한 내용이다.
성능은 시스템이나 애플리케이션이 주어진 자원 내에서 작업을 얼마나 효율적으로 처리하는지를 측정하는 개념이다. 이는 사용자 만족도와 서비스 품질에 직접적인 영향을 미치며, 성능이 높을수록 짧은 응답 시간과 높은 처리량을 제공해 자원을 효율적으로 사용하는 것이 목표이다.
성능을 측정하고 분석하기 위해 다양한 지표가 사용되는데, 대표적인 성능 지표는 다음과 같다.
성능 지표를 분석함으로써 시스템이 어떻게 작동하는지 이해하고, 개선이 필요한 부분을 식별할 수 있다.
성능 최적화는 시스템의 효율성과 사용자 만족도를 높이기 위해 필수적이다. 주요 목표는 다음과 같다:
병목 현상
은 시스템의 특정 부분이 전체 성능을 저하시키는 현상을 의미한다. 하나의 구성 요소가 비효율적으로 작동하면 시스템 전체의 처리 속도가 느려질 수 있다. 이러한 현상은 시스템 자원의 불균형 사용으로 인해 발생하며, 성능 최적화의 주요 대상이 된다.
병목 현상은 여러 가지 원인에 의해 발생할 수 있다. 주요 원인은 다음과 같다:
CPU 병목: CPU가 과부하 상태일 경우 작업 처리 속도가 저하된다. 과도한 계산 작업이나 비효율적인 코드가 원인이 될 수 있다.
예시
: 넷플릭스는 방대한 콘텐츠를 사용자에게 제공하기 위해 추천 알고리즘을 실행한다.
사용자가 선호할 만한 영상을 추천하는 과정에서 복잡한 기계 학습 모델이 사용된다.
만약 특정 시간대에 많은 사용자가 동시에 접속해 추천 알고리즘이 과도한 연산을 요구하면, 서버의 CPU가 과부하 상태에 빠져 전체 응답 속도가 느려질 수 있다.
이때 CPU 병목이 발생하며, 사용자가 스트리밍을 시작할 때 지연이 생길 위험이 있다.
메모리 병목: 메모리 사용이 한계에 도달하면 성능이 급격히 떨어진다. 메모리 누수나 대용량 데이터 처리가 원인이 될 수 있다.
예시
: 국민은행, 카카오뱅크와 같은 시중 은행 및 인터넷 은행의 송금 시스템에서 대량의 거래 데이터를 실시간으로 처리할 때, 메모리를 효율적으로 사용하지 못하면 문제가 생길 수 있다.
예를 들어, 수많은 사용자 거래를 메모리에 유지하면서 여러 검증 작업을 동시에 수행할 경우 메모리 한계에 도달해 성능이 급격히 저하될 수 있다.
메모리 누수나 비효율적인 데이터 구조 사용이 원인일 수 있으며, 이런 상황에서는 실시간 송금이 지연되거나 오류가 발생할 위험이 있다.
네트워크 병목: 네트워크 대역폭이 부족하거나 데이터 전송 속도가 느릴 때 발생한다. 대량의 데이터 전송이 필요한 작업이 주된 원인이다.
예시
: 페이팔 같은 결제 서비스는 여러 국가의 은행과 통신해 거래를 처리한다.
만약 페이팔 서버가 해외 은행과의 네트워크 연결 속도가 느려지면 사용자 결제 승인 과정이 지연될 수 있다.
특히 블랙프라이데이 같은 대규모 이벤트 때 대량의 결제 요청이 몰리면 네트워크 병목이 발생할 가능성이 높다.
이 병목은 사용자에게 결제 대기 화면을 보여주며, 거래가 정상적으로 완료되지 않아 불편함을 초래할 수 있다.
I/O 병목: 디스크 읽기/쓰기 속도가 느릴 때 성능 저하가 발생한다. 데이터베이스나 파일 입출력이 많을 때 흔히 나타난다.
예시
: AWS에서 호스팅된 웹 애플리케이션이 대량의 로그를 S3 같은 스토리지에 지속적으로 기록할 때, 디스크 쓰기 속도가 느리면 전체 애플리케이션 성능이 저하될 수 있다.
대량의 데이터베이스 읽기/쓰기 작업이 동시에 수행되면 I/O 병목이 발생할 수 있다.
이런 문제는 클라우드 환경에서 특히 중요하며, 파일 시스템이 제대로 최적화되지 않으면 서비스의 응답 시간이 길어질 수 있다.
병목 지점을 식별하기 위해 다음 방법을 사용할 수 있다:
top
, htop
, iostat
, vmstat
같은 시스템 모니터링 도구를 사용해 자원의 사용률을 확인하고, CPU, 메모리, 네트워크, I/O 상태를 실시간으로 관찰한다.이 도구들(예: JProfiler, VisualVM, ELK Stack, Splunk)은 성능 모니터링 및 분석 도구에 해당한다. 성능 테스트는 부하를 생성해 시스템의 한계를 평가하는 과정이며, VisualVM 같은 도구는 테스트가 끝난 후 병목 지점을 식별하거나 리소스 사용을 분석하는 데 사용된다.
워크로드
란, 가장 일반적인 의미에서 시스템이나 네트워크가 작업을 완료하거나 특정 출력을 생성하는 데 걸리는 시간과 사용되는 컴퓨터 리소스를 말한다. - IBM
성능 테스트는 특정 워크로드에서 소프트웨어의 응답 시간, 안정성, 확장성, 가용성 등을 평가하여 고객에게 최적의 소프트웨어 성능을 제공할 수 있는지를 측정하는 과정이다.
예상 목표 TPS를 달성할 수 있는지.
피크 시간대에도 서비스가 원활히 제공되는지.
장시간 운영 시 자원 누수 없이 안정적으로 작동하는지.
사용자가 증가할 경우 수평/수직 확장을 위한 기준이 무엇인지.
일부 시스템이 다운되더라도 최소한의 리소스로 서비스가 유지되는지.
시스템 리소스가 과도하게 할당되지 않았는지.
시스템 메트릭이 시스템 상태를 정확히 설명하는지.
최적의 하드웨어 및 소프트웨어 설정값이 무엇인지.
트랜잭션
(Transaction): DB 트랜잭션과는 다르게, 논리적인 업무 요청의 단위로, 사용자가 한 번의 요청을 보내는 행위.총 사용자
(Total User): 현재 서비스 요청자 + 대기자 + 비접속자.현재 서비스 요청자
(Active User): 현재 서비스에 요청을 보낸 사용자.서비스 대기자
(Inactive User): 서비스에 접속해 있지만 아직 요청을 보내지 않은 사용자.동시 사용자
(Concurrency User): Active User + Inactive User.처리량
(Throughput): 단위 시간당 처리되는 작업의 양 (TPS: 초당 트랜잭션 수, RPS: 초당 요청 수).응답 시간
(Response Time): 사용자가 서버에 요청을 보낸 후 응답을 받기까지 걸리는 시간.요청 주기
(Request Interval): 요청 주기는 응답 시간 + 생각 시간 + 조작 시간.생각 시간
(Think Time): 사용자가 응답을 받은 후 화면을 보며 생각하는 시간.조작 시간
(Operation Time): 사용자가 데이터를 입력하거나 화면을 채우는 시간 (예: 로그인 시 아이디와 비밀번호를 입력하는 시간).성능 테스트 과정은 아래의 크게 4가지로 구분되어 진행된다.
테스트 항목
: 현재 테스트할 모든 애플리케이션을 나열하고, 우선순위를 기준으로 테스트 범위를 정한다.
테스트 조건
: 각 애플리케이션이 충족해야 할 조건을 설정한다.
테스트 시나리오
: 워크로드 모델을 작성하여, 업무 중요도가 높은 항목을 선정하고 가설을 세워 성능 테스트 대상을 설정한다. (워크로드 모델, 목표 TPS, 사용자 수, 응답 시간(sec), 생각 시간, 결과 TPS)
점검 항목
: TPS, 응답 시간, 시스템 메트릭, 에러 발생률, 네트워크 사용률, 로드 밸런서 부하 등을 점검한다.
(출처: IBM: Aspects of Performance Tuning)
A: 사용자가 증가되는 시점 (Ramp Up)
Saturation point: 임계지점(포화지점)
사용자가 증가해도 더 이상 처리량이 증가되지 않는 상태가 되는 시점
이 상태가 되면 현재 시스템이 처리할 수 있는 최대 Capacity 에 도달했음을 나타낸다.
B: 최대 부하 지점
사용자가 증가해도 처리량이 일정하게 유지되는 지점
이때 그래프가 고르고 안정적이라면 서비스가 최대 Capacity로 유지될 수 있음을 나타낸다.
C: Buckie 영역(성능 감소지점)
최대 처리를 더 이상 견뎌내니 못하고 성능이 감소되는 지점
이 시점은 시스템의 한계를 초과했을 때 혹은 네트워크 대역폭을 전부 사용한 경우에 이런 현상이 발생한다.
성능테스트 방법에 따라 그래프 패턴을 확인할 수 있다.
(자세한 내용은 Grafana Labs - Load test types 에서 확인한다)
Smoke 테스트
는 스크립트가 제대로 작동하는지와 시스템이 최소한의 부하에서 적절하게 성능을 발휘하는지를 검증한다.
Average Load 테스트
는 시스템이 예상되는 정상적인 조건에서 어떻게 작동하는지를 평가한다.
Stress 테스트
는 부하가 예상 평균을 초과할 때 시스템이 한계에서 어떻게 작동하는지를 평가한다.
Soak 테스트
는 시스템의 신뢰성과 성능을 장시간에 걸쳐 평가한다.
Breakpoint 테스트
는 부하를 점진적으로 증가시켜 시스템의 용량 한계를 식별한다.
Spike 테스트
는 갑작스럽고 짧으며 대규모로 증가하는 활동에서 시스템의 동작과 생존 여부를 검증한다.
워크로드 모델링
은 성능 테스트 대상 워크로드를 나열하고, 업무 중요도가 높은 순서로 가설을 설정해 성능 테스트 대상을 선정하는 작업이다.
시스템에서 사용하는 모든 워크로드를 나열한 후, 가장 많이 사용되는 70~80% 의 워크로드만을 성능 테스트 대상으로 삼는다.
워크로드 | Target TPS | User | Response Time (sec) | Think Time | TPS |
---|---|---|---|---|---|
메인화면 | 20 | 60 | 2 | 1 sec | 20 |
로그인 | 15 | 45 | 1 | 2 sec | 15 |
마이페이지 | 10 | 50 | 3 | 2 sec | 10 |
상품 | 25 | 75 | 2 | 1 sec | 25 |
이체 | 10 | 70 | 4 | 3 sec | 10 |
혜택 | 8 | 32 | 2 | 2 sec | 8 |
알림 | 12 | 36 | 1 | 2 sec | 12 |
Total | 100 | 368 | - | - | 100 |
위 워크로드는 7개의 주요 애플리케이션을 나열한 것이다. (실제 워크로드는 이보다 많지만, 여기서는 약 80%의 주요 워크로드만 모델링했다.)
표에서 사용된 용어들은 다음과 같다. (위에 있는 “3.3 성능 테스트의 주요 용어”와 같은 내용이다)
Target TPS
: 목표로 하는 초당 트랜잭션 수.
User
: 가상 사용자 수로, 예상되는 사용자 수를 설정한다.
Reponse Time
: 응답 시간.
TPS
: 실제로 측정된 처리량.
위와 같은 워크로드 모델을 구성하여 성능 테스트를 진행하고, 각 워크로드에 부하를 균등하게 분산해 주입할 방안을 결정한다.
오픈 소스로 공개된 성능 테스트 도구들 중 nGrinder와 K6에 대해 정리했다.
이번 글에서는 성능의 기초 개념, 병목 현상, 그리고 성능 테스트에 대해 간단하게 정리해보았다. 이러한 개념을 이해함으로써 시스템의 성능을 최적화하고 문제를 사전에 예방하는 데 필요한 기반 지식을 다질 수 있었다. 글을 작성하면서 성능 테스트와 함께 활용해야 할 모니터링 도구의 중요성도 새롭게 느끼게 되었는데, 이 부분은 다음 글에서 성능 테스트와 함께 더 깊이 다뤄볼 예정이다.
추가로, 앞으로 작성할 글에서는 Java/Spring Boot 환경에서 성능을 개선하기 위해 고려할 수 있는 요소들을 다루어 보려 한다.
Linux Performance Analysis in 60,000 Miliseconds (리눅스 서버에서 성능 이슈가 발생했을 때, 첫 60초 안에 확인해야할 사항)
Linux Systems Performance (넷플릭스 - 시니어 성능 엔지니어 Brendan Gregg)
최근 내가 하고 있는 일이 너무 많아서 머릿속이 복잡해졌다. 그래서 생각을 정리하고자 2024년 3분기 회고 글을 작성하게 되었다.
지금 내가 생활할 수 있는 기반을 제공하는 회사가 무엇보다 중요하다고 생각한다.
최근에는 알림 시스템
을 설계하고 운영하기 위해 필요한 기술을 공부 중이다.
다른 회사들의 사례를 참고하기 위해 기술 블로그와 영상을 찾아보고 있다.
현재는 Kafka를 배우는 중인데, 양이 방대하고 내 경력에서는 쉽지 않은 분야지만 조금씩 익숙해지려 노력하고 있다.
리팩터링
기존 서비스의 백엔드 코드를 리팩터링해 유지보수하기 쉽게 개선하고 있다.
최근 읽고 있는 좋은 코드, 나쁜 코드
와 이전에 읽은 내 코드가 그렇게 이상한가요?
가 많은 도움을 주었다.
리팩터링에는 정답이 없다고 생각하지만, 적어도 팀원이나 제3자가 이전보다 쉽게 이해할 수 있도록 만드는 것이 중요하다고 본다.
비록 지금 회사에서 오래 일하지는 않았지만, 주도적인 자세가 중요하다는 점을 많이 깨닫고 있다.
글또가 무엇인지 알고 싶다면 여기를 클릭해 주시면 됩니다.
이전 9기에 이어 이번 10기에도 참여하게 되었다.
사실 참여를 결정할 때, ‘과연 내가 2주에 한 번씩 글을 작성할 수 있을까?’ 하는 걱정이 있었지만,
이번 10기가 마지막이기도 하고, 글을 작성하는 것 외에도 많은 장점이 있어서 참가하게 되었다.
https://leetcode.com/problems/ransom-note/
주어진 ransomNote가 magazine의 글자를 이용하여 만들어질 수 있는지 확인하는 문제이다.
매거진에 있는 각 문자는 한 번만 사용할 수 있기 때문에
ransomNote의 각 문자를 magazine에서 충분히 가져올 수 있는지 검사하는 것이 핵심이다.
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
HashMap<Character, Integer> map = new HashMap<>();
int len = magazine.length();
for(int i = 0; i < len; i++) {
char current = magazine.charAt(i);
map.put(current, map.getOrDefault(current, 0) + 1);
}
len = ransomNote.length();
for(int i = 0; i < len; i++) {
char key = ransomNote.charAt(i);
if(map.get(key) != null && map.get(key) > 0) {
map.put(key, map.get(key) - 1);
} else {
return false;
}
}
return true;
}
}
이 코드는 매거진과 ransomNote의 길이를 각각 한 번씩 순회하며
각 문자의 개수를 세고 비교하므로 시간 복잡도는 O(m + n) (m: 매거진의 길이, n: 랜섬노트의 길이) 이다.
[1] 매거진의 문자 개수 세기
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// ...
HashMap<Character, Integer> map = new HashMap<>();
int len = magazine.length();
for (int i = 0; i < magazine.length(); i++) {
char current = magazine.charAt(i);
map.put(current, map.getOrDefault(current, 0) + 1);
}
// ...
}
}
여기서 HashMap을 사용하여 매거진에 등장하는 각 문자의 개수를 기록한다.
map.getOrDefault(current, 0) + 1
은 현재 문자가 map에 이미 존재하면 그 값을 1 증가시키고, 그렇지 않으면 0을 기본값으로 설정하여 1로 초기화한다.
[2] ransomNote에서 각 문자를 매거진에서 찾기
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// ...
len = ransomNote.length();
for(int i = 0; i < ransomNote.length(); i++) {
char key = ransomNote.charAt(i);
if(map.get(key) != null && map.get(key) > 0) {
map.put(key, map.get(key) - 1);
} else {
return false;
}
}
// ...
}
}
이 반복문에서는 ransomNote에 있는 각 문자를 하나씩 확인한다.
map.get(key) != null && map.get(key) > 0
은 매거진에 해당 문자가 존재하고, 아직 사용 가능한 문자의 개수가 남아 있는지 확인한다.
해당 문자가 매거진에 있다면 map.put(key, map.get(key) - 1)
로 해당 문자의 개수를 1 감소한다.
만약 문자가 더 이상 없거나 매거진에 존재하지 않으면 즉시 false
를 반환하여 ransomNote를 만들 수 없음을 알린다.
import java.util.HashMap;
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// 1. HashMap을 이용해 magazine의 각 문자의 개수 기록
HashMap<Character, Integer> map = new HashMap<>();
for (int i = 0; i < magazine.length(); i++) {
char current = magazine.charAt(i);
map.put(current, map.getOrDefault(current, 0) + 1);
}
// 2. ransomNote의 각 문자를 검사
for (int i = 0; i < ransomNote.length(); i++) {
char current = ransomNote.charAt(i);
// map에 문자가 없거나, 개수가 부족하면 false 반환
if (!map.containsKey(current) || map.get(current) == 0) {
return false;
}
// 사용한 문자의 개수를 줄임
map.put(current, map.get(current) - 1);
}
// 3. 모든 문자를 확인한 후 true 반환
return true;
}
}
[1] magazine의 문자와 등장 횟수를 HashMap에 기록
매거진의 각 문자를 순회하면서, 각각의 문자가 몇 번 등장하는지를 HashMap에 기록한다.
이때, charAt() 메서드를 사용하여 magazine의 각 문자에 접근한다.
[2] ransomNote의 각 문자를 HashMap에서 확인
ransomNote의 각 문자를 확인하며, 그 문자가 HashMap에서 충분히 있는지 확인한다.
HashMap에서 문자가 있는지 확인하고, 있다면 그 문자의 개수를 줄여준다.
만약 문자가 없거나 충분하지 않다면, 바로 false를 반환한다.
HashMap의 개념과 문법을 알아야 문제를 쉽게 풀 수 있다.
첫 번째 풀이가 Runtime 시간이 10ms로 두 번째 풀이보다 6ms 더 빠르다.
if(map.get(key) != null && map.get(key) > 0)
https://leetcode.com/problems/implement-queue-using-stacks/description/
두 개의 스택을 사용해 큐(FIFO)를 구현하는 문제였다.
스택의 기본 연산만 사용하여 큐를 구현하라는 것이다.
큐에서의 주요 연산인 push(), pop(), peek(), empty()를 구현해야 한다.
class MyQueue {
Stack<Integer> s1;
Stack<Integer> s2;
public MyQueue() {
s1 = new Stack<>();
s2 = new Stack<>();
}
// 큐의 뒤쪽에 요소를 추가하는 연산
public void push(int x) {
s1.push(x);
}
// 큐의 앞쪽 요소를 제거하는 연산
public int pop() {
// s2가 비어 있을 때 s1의 모든 요소를 s2로 옮긴 후 pop
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
}
return s2.pop();
}
// 큐의 앞쪽 요소를 확인하는 연산
public int peek() {
// s2가 비어 있을 때 s1의 모든 요소를 s2로 옮긴 후 peek
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
}
return s2.peek();
}
// 큐가 비어 있는지 확인하는 연산
public boolean empty() {
return s1.isEmpty() && s2.isEmpty();
}
}
스택은 LIFO 방식이지만, 큐는 FIFO 방식을 요구합니다. 이를 해결하기 위해 두 개의 스택을 사용할 수 있다.
스택1(s1): 새로운 요소를 추가하는 스택.
스택2(s2): 요소를 제거하거나 앞쪽을 확인할 때 사용하는 스택.
이 두 스택을 사용해 큐의 동작을 흉내내면 된다.
s1에 새 요소를 계속 추가하고,
요소를 제거하거나 볼 때는 s1에서 s2로 요소를 옮기고, s2의 상단에서 요소를 제거하거나 확인할 수 있다.
이렇게 하면 FIFO 동작을 구현할 수 있다. (그림으로 확인하면 더 이해하기 쉽다 !)
2개의 스택을 이용하여 큐를 구현하는 문제였다.
구현에 대한 아이디어가 한 번에 떠오르지 못해서, 힌트를 보고 30분 안으로 구현해서 성공했다. 동작 원리는 알고 있었지만, 막상 구현하기엔 쉽지 않았다. 이 문제는 일주일 내로 다시 풀어보기로 하자 !
그리고 머릿속으로 정확히 안떠오르면 아이패드로 그림을 그려가면서 해보자. 그럼 더 쉬울 거다 !
책의 내용 중 자극히 개인적으로 중요한, 기억하고 싶은 부분들을 메모해두자. 자세한 내용은 책을 참고하자.
실제 서비스되는 환경에서 실행되는 소프트웨어가 되기까지의 과정
코드를 작성할 때 다음과 같은 네 가지 상위 수준의 목표를 달성해야 한다.
코드 품질
의 여섯 가지 핵심 요소는 다음과 같다.
단일 함수 내에서 너무 많은 작업을 수행하면 코드를 이해하기 어렵게 만드는 문제가 발생할 수 있다.
그러한 문제가 발생한다면 함수를 작게 만들고 수행하는 작업을 명확하게 해서 코드의 가독성과 재사용성을 높힌다.
줄 수
(number of lines): ‘한 클래스는 코드 300 줄을 넘기지 않아야 한다’와 같은 가이드라인을 접하는 경우가 있는데, 이는 경고의 역할만 할 뿐, 본인이 속한 팀의 문화에 따라 다를 수 있다.
응집력
(cohesion): 한 클래스 내의 모든 요소들이 얼마나 잘 속해 있는지를 보여주는 척도로, 좋은 클래스는 응집력이 매우 강하다. 응집력에는 여러 방식이 있다.
순차적 응집력
이 있고, 케이크를 만들기 위해 필요한 장비를 모이는 과정과 같은 기능적 응집력
이 있다.관심사의 분리
(seperation of concerns): 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다고 주장하는 설계 원칙이다.
코드를 적절한 크기의 클래스로 쪼개지 않으면 너무 많은 개념을 한꺼번에 다루고,
그로 인해 가독성이 떨어지며 모듈화가 덜 이루어지고,
재사용과 일반화가 어렵고, 테스트하기도 어려워진다.
하나의 추상화 계층에 대해 두 가지 이상의 다른 방식으로 구현을 하거나 향후 다르게 구현할 것으로 예상되는 경우 인터페이스를 정의하는 것이 좋다.
이를 통해 코드를 더욱 모듈화할 수 있고 재설정도 훨씬 쉽게 할 수 있다.
모든 것을 위한 인터페이스?
주어진 추상화 계층에 대해 하나의 구현만 존재하는 경우에도 인터페이스를 통해 추상화 계층을 표현해야 하는지는 팀에서 결정할 문제다.
많은 소프트웨어 공학 철학에서는 인터페이스 사용을 권장하지만, 나는 조금 다른 생각을 가지고 있다.
나의 경우, 두 가지 이상의 구현이 있거나 앞으로 다른 구현이 필요할 가능성이 있는 경우가 아니라면, 단 하나의 구현 클래스만 있다면 굳이 인터페이스를 만들 필요는 없다고 본다.
인터페이스와 구현 클래스가 1:1로 대응될 때는 서비스가 확장될수록 관리해야 할 파일과 코드가 불필요하게 많아지기 때문이다.
또한, 다른 개발자가 코드를 이해하려고 할 때, 인터페이스 -> 구현 클래스(하위) 로 이동하면서 찾아야 한다.
그래서 나는 일반적으로 먼저 구현 클래스로 시작하고, 인터페이스가 필요한 시점에 도입하여 설계를 보완해 나가는 방식을 선호한다.
일반적으로 클래스를 작성하거나 수정해야 할 때, 인터페이스를 붙이는 것이 어렵지 않도록 코드를 작성해야 한다.