일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 자켓실측
- 와끼
- 비슬론지퍼
- 엑셀필터복사붙여넣기
- MERN스택
- 영어시간읽기
- Armhole Drop
- WHATTIMEOFTHEDAY
- 나일론지퍼
- 40HQ컨테이너
- 필터링후복사붙여넣기
- 엑셀드래그단축키
- 40HQ컨테이너40GP컨테이너차이
- 미니마카
- 핸드캐리쿠리어차이점
- 미국영어연음
- 웹API
- 고급영어단어
- 암홀트롭
- AATCC
- 우레탄지퍼
- 봉제용어
- 지연환가료
- 요척합의
- 클린코드
- 슈퍼코딩
- 비리짐
- TACKING
- 엑셀자동서식
- 헤이큐
- Today
- Total
CASSIE'S BLOG
육각형 개발자 책 정리 본문
자카르타 프로젝트
자바를 위한 오픈소스 소프트웨어를 만들고 정리하는 프로젝트로 지금도 활발하게 운영하고 있다.
자카르타 프로젝트에서 진행하는 유명한 오픈소스 중 하나가 바로 톰캣 서버다.
또한, 서블릿, JPA 등 웹 개발에 사용되는 스팩의 관리 주체가 오라클에서 이클립스 재단으로 이전되었다.
서블릿은 웹 서버에서 동작하며, 클라이언트의 요청을 처리하고 동적인 웹 페이지를 생성하는 자바 프로그램입니다.
개발은 회사가 나에게 돈을 벌어주는 기능을 만드는 과정이기도 했다. 회사 규모가 작을수록 개발 결과물이 회사가 생존하는데 큰 영향을 주었다.
주니어 개발자가 시니어 개발자로 성장하기 위해서는 다양한 역향을 익혀야한다.
1. 구현 기술
2. 설계 역량
3. 업무 관리와 요구 분석, 공유, 리드 & 팔로우
물류 시스템 개발은 왜 망했을까?
앞서 출시도 못하고 실패한 물류 시스템 개발 경험을 얘기했다. 이 소프트웨어는 왜 출시 조차 못 했을까? 지금 생각해보면 가장 큰 문제는 개발 리더 부재였다. 나와 J씨 둘 다 신 입이었지만 오로지 2명만 개발에 참여했다. 우리는 정말 열심히 코딩했다. 하지만 제품을 만들기 위해 필요한 다른 활동에 소홀했다. 아니 정확하게 말하면 코딩 외에 어떤 일을 해 야 하는지 몰랐다.
구현해야 할 기능은 복잡했다. 신입 개발자 2명이 기능 요구 사항을 분석한다는 것은 버 거운 일이다. 요구 사항을 이해하기 위한 회의는 개발 초기 한두 번 진행한 것이 전부였고 그마저도 경력 개발자와 기획자는 참석하지도 않았다. 요구 사항 분석이라는 첫 단추부터 틀어진 것이다.
일정 관리도 엉망이었다. 얼마나 진척됐는지 아무도 알지 못했다. 둘이 알아서 하라는 식 이었다. 관리자는 우리에게 "잘 되고 있지?”라고 묻는 게 다였고 그저 "네"라고 답할 뿐이 었다. 그때는 실제로 잘 진행되고 있다고 생각했다. 하지만 착각이었다
프레임워크와 라이브러리도 배워야 한다. 프로그래밍 언어의 기본 기 능만 사용해서 개발하는 경우는 거의 없다. 예를 들어 자바로 웹 서비스를 개 발하면 스프링 부트spring Bool, 하이버네이트Hlberate, 마이바티스wyeatis 와 같은 기 술을 사용한다. 자체적으로 APT를 개발할 때 필요한 기능을 제공하는 고 언어 coong Gin과 같은 프레임워크를 사용해서 웹 서비스를 개발한다. 자바스크 립트 라이브러리인 리액트pod를 사용해서 웹 애플리케이션을 만들 때도 다양 한 라이브러리를 함께 사용한다. 프레임워크와 라이브러리의 역할이 커짐에 따라 언어보다 더 많이 익혀야 하는 대상이기도 하다.
하이버네이트는 자바 객체를 데이터베이스 테이블에 자동으로 매핑해주는 ORM 프레임워크입니다.
JPA는 표준 인터페이스이고, 하이버네이트는 그 구현체이므로 대체하는 관계가 아니라 하이버네이트는 JPA를 구현하여 사용하는 것입니다.
보통 하이버네이트를 JPA와 함께 사용합니다. JPA의 표준 인터페이스를 사용하여 코드의 이식성을 높이고, 하이버네이트의 구체적인 구현을 통해 다양한 기능을 활용할 수 있습니다.
1. @GenericGenerator: 커스텀 식별자 생성기를 정의할 때 사용됩니다.
@Entity
public class User {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
// ...
}
2. @Type: 필드의 사용자 정의 데이터 타입을 매핑할 때 사용됩니다.
@Entity
public class Event {
@Id
@GeneratedValue
private Long id;
@Type(type = "org.hibernate.type.LocalDateType")
private LocalDate eventDate;
// ...
}
3. @Cache: 엔티티의 캐시 설정을 정의할 때 사용됩니다.
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
// ...
}
4. @BatchSize: 컬렉션이나 엔티티를 페치할 때 배치 크기를 지정합니다.
@Entity
public class Company {
@Id
@GeneratedValue
private Long id;
@OneToMany
@BatchSize(size = 10)
private Set<Employee> employees;
// ...
}
5. @Formula: 가상 속성을 정의할 때 사용되며, 데이터베이스에서 계산된 값을 가져올 수 있습니다.
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String firstName;
private String lastName;
@Formula("concat(firstName, ' ', lastName)")
private String fullName;
// ...
}
이 어노테이션들은 JPA 표준 어노테이션을 보완하고, 하이버네이트의 고유 기능을 활용할 수 있게 해줍니다.
개발자가 어떤 역할(또는 직군)을 하는지에 따라 선택해야 하는 기술이 달라 진다. 자바를 주력으로 하는 백엔드 개발자라면 스프링, 마이바티스, JPA, 톰캣Tomcat과 같은 기술을 선택할 것이고 모바일 게임 개발자는 C#과 유니티unity 를 구현 기술로 선택할 수 있을 것이다.
마이바티스는 SQL 쿼리를 XML 파일에 작성하고 자바 객체와 매핑하여 데이터베이스 작업을 쉽게 수행할 수 있게 해주는 퍼시스턴스 프레임워크입니다.
요즘에 마이바티스 안 쓰지? jpa 쓰지? 대신에?
맞습니다. 최근에는 JPA와 하이버네이트가 더 많이 사용되고 있지만, 마이바티스도 여전히 사용됩니다. JPA는 객체 지향적인 접근을 제공하여 코드의 이식성을 높이는 반면, 마이바티스는 SQL 쿼리를 직접 작성할 수 있어 세밀한 제어가 필요할 때 유용합니다. 그래서 상황에 따라 두 기술 모두 선택될 수 있습니다.
동시에 쓰는 경우도 있어?
네, 마이바티스와 JPA를 동시에 사용하는 경우도 있습니다. 예를 들어, JPA로 기본적인 CRUD 작업을 처리하면서, 복잡한 쿼리가 필요할 때 마이바티스를 사용하는 방식입니다. 이렇게 하면 두 기술의 장점을 살릴 수 있습니다.
사용할 구현 기술도 자신이 속한 기술 조직의 방침이나 개인의 취향에 따라진다.
예를 들어 스프링 부트를 사용하는 개발자 중에서 어떤 개발자는 템플릿 엔진으로 타임리프를 선택할 것이도 또 다른 개발자는 본인의 기호에 따라 프리마커를 템플릿 엔진으로 사용할 것이다.
타임리프는 서버 측에서 HTML 템플릿을 렌더링하는 템플릿 엔진이고, 톰캣은 자바 서블릿과 JSP를 실행하는 웹 서버입니다.
자바 서블릿은 클라이언트 요청을 처리하고 동적인 웹 콘텐츠를 생성하는 자바 프로그램이며,
JSP(JavaServer Pages)는 HTML 안에 자바 코드를 삽입하여 동적인 웹 페이지를 작성하는 템플릿 기술입니다.
네, 맞습니다. JSP와 타임리프는 모두 동적인 웹 페이지를 생성하는 역할을 하며, HTML 템플릿 내에서 데이터를 표시하는 데 사용됩니다.
하지만 JSP는 자바 코드를 HTML에 직접 삽입하는 방식인 반면,
타임리프는 HTML과 분리된 템플릿 언어를 사용하여 더 깔끔하고 유지보수하기 쉬운 코드를 제공합니다.
학습하려고 하는 구현 기술을 정할 때 다음 2가지 기준을 적용하면 좋다.
• 현재 사용 중인 기술
• 문제를 해결하기 위한 기술
현재 사용 중인 기술은 당연히 학습해야 한다. 지금 당장 잘 활용하는 것이 중 요하기 때문이다. 예전에 D사에 다닐 때의 일이다. 전사에서 공통으로 사용할 서비스 개발에 참여한 적이 있다. 이 프로젝트는 스트럿츠Struts, 스프링Spring, 아이바티스IBatis, 벨로시티Velocity를 구현 기술로 사용했다. 스프링을 제외한 나 머지 기술은 이전에 다뤄본 경험이 없었기 때문에 프로젝트 초기에 각 기술의 레퍼런스 문서와 관련 책을 읽어가며 개발했다.
현재 사용 중인 기술이 본인의 취향과 맞지 않더라도 코드를 이해하고 수정할 수 있을 정도의 지식은 있어야 한다. 예전에 기간계 프로그램의 권한 관리 기 능을 재구현하는 프로젝트에 참여한 적이 있는데 이 프로젝트의 프런트 개발 에 사용한 0 O 플랫폼은 개인적으로 선호하지 않는 도구였다. 하지만 프로젝 트를 제대로 진행하려면 0 0플랫폼 기술을 써야 했기 때문에 이미 존재하는 기존 코드와 공식 문서를 보면서 기술을 익혔다.
힙 덤프:
특정 시점의 힙 상태를 파일로 복사해서 저장하는 것을 말하며 힙 덤프 파일 분석으로 어디서 메모리를 많이 사용하는지 파악할 수 있다.
기술 이해 부족으로 발생한 문제 2
어느 날 동료 개발자가 찾아와 내게 도움을 요청했다. 스프링이 제공하는 @Transactional 애너테이션Annotation을 사용해서 트랜잭션을 처리하고 있는데 유독 한 코드만 롤백rollback 이 안 된다고 했다. 문제 코드를 봤는데 당연히 안 될 수밖에 없었다. 코드는 다음과 같은 형태를 띄었다.
public void someMethod() {
...
this.internalMethod();
...
}
@Transactional
private void internalMethod() {
...
}
스프링에서 트랜잭션 범위를 지정할 때 사용하는 @Transactional 애너테이션은 intemalMethod()에 붙어 있다. 이 코드를 만든 개발자는 트랜잭션 처리를 하기 위해서는@Transactional 애너테이션을 사용하면 된다는 것을 알고 있었지만 스프링 AOP와 프록시에 대한 이해는 부족했던 것 같다. 그래서 private 메서드인 internalMethod() 메 서드에 @Transactional 애너테이션을 적용했고 롤백이 되지 않는 문제가 발생한 것이다.
스프링의 @Transactional 애너테이션이 제대로 작동하지 않는 이유는 다음과 같습니다:
- 프록시 메커니즘: 스프링은 @Transactional을 적용하기 위해 프록시라는 대리 객체를 사용합니다.
- 프록시 적용 범위: 프록시는 public 메서드에만 적용됩니다.
- private 메서드 문제: private 메서드에 @Transactional을 붙이면 프록시가 적용되지 않아서 트랜잭션이 동작하지 않습니다.
따라서, 트랜잭션을 적용하려면 @Transactional 애너테이션을 private 메서드가 아닌 public 메서드에 붙여야 합니다.
public void someMethod() {
this.internalMethod(); // 프록시를 거치지 않음
}
@Transactional
public void internalMethod() { // 프록시를 통해 트랜잭션 적용
// 트랜잭션 처리
}
스프링 AOP와 프록시 메커니즘
스프링에서 @Transactional 애너테이션은 AOP를 사용하여 트랜잭션 경계를 설정합니다. 이를 위해 스프링은 대상 객체를 감싸는 프록시 객체를 생성합니다. 이 프록시 객체는 메서드 호출을 가로채고, 트랜잭션을 시작하거나 종료하는 로직을 삽입합니다.
AOP(Aspect-Oriented Programming)는 관점 지향 프로그래밍입니다. 주요 개념은 다음과 같습니다:
Aspect: 공통 기능(예: 트랜잭션, 로깅)을 모듈화한 것.
Advice: 실행될 부가 기능의 코드.
Join Point: Advice가 실행될 지점(메서드 호출 등).
Proxy: 실제 객체를 감싸서 메서드 호출을 가로채고 Advice를 적용하는 대리 객체.
즉, AOP는 핵심 로직과 부가 기능을 분리해서 코드를 깔끔하게 유지할 수 있게 해줍니다.
프록시(Proxy)는 실제 객체를 대신해서 메서드 호출을 가로채는 대리 객체입니다. 여기서 Advice는 가로챈 후에 실행할 추가 작업(예: 트랜잭션 시작/종료, 로깅 등)입니다.
간단한 비유:
- 실제 객체: 가게 주인.
- 프록시: 가게 주인을 대신해서 손님을 맞이하는 직원.
- Advice: 직원이 손님을 맞이할 때 하는 추가 작업(예: 인사, 계산).
즉, 프록시는 메서드 호출을 대신 처리하면서 필요한 추가 작업(Advice)을 수행합니다.
지금까지 여러 언어가 탄생했지만 자바는C 언어와 함께 여전히 주요 개발 언어로 쓰이고 있다. 스프링 프레임워크는 첫 출시 이후 지금까지 가장 인기 있는 자바 서버 개발 프레임워크다. 마이바티스 와 JPA(하이버네이트)도 오랜 기간 데이터베이스 연동 기술로 사용되고 있다.
서버 역할을 하는 톰캣과 아파치Apache도 여전히 사용되고 있다. 내 경험으로만 한정해도 사용하던 기술 변화가 꽤 있었고 개발 업계 전체로 살펴보면 더 많은 기술이 등장하고 사라지는 과정이 반복됐다. 대체 기술은 계속해서 생겨난다.
리액트가 높은 시장 점유율을 차지하고 있는 와중에도 스벨트Svelte 같은 새로운 방식의 기술이 나오고 있으며 고 언어와 러스트Rust이 같은 개발 언어도 꾸준 히 등장해서 기존 언어를 위협하고 있다.
주니어 개발자라면 유행과 상관없는 지식을 1년에 1개 이상 학습하자.
HTTP프로토콜, 네트워크 프로그래밍 기초, 동시성처리, 프로그래밍 언어 등 유행에 상과없이 개발의 기초가 되는 중요한 내용이 많다.
동시성 처리는 여러 작업을 동시에 수행하는 것을 말합니다. 이는 프로그램이 동시에 여러 작업을 처리하거나, 하나의 작업을 여러 부분으로 나눠 병렬로 처리할 수 있게 합니다.
동시성 처리에 대한 간단한 설명
- 멀티스레딩: 여러 스레드가 동시에 실행되어 작업을 처리.
- 멀티프로세싱: 여러 프로세스가 동시에 실행되어 작업을 처리.
- 비동기 프로그래밍: 작업을 비동기로 실행하여 기다림 없이 다음 작업을 진행.
동시성 처리의 기초 공부 주제
- 스레드와 프로세스: 기본 개념과 차이점.
- 스레드 동기화: 스레드 간의 자원 공유와 충돌 방지 (예: 뮤텍스, 세마포어).
- 멀티스레딩 프로그래밍: 스레드를 생성하고 관리하는 방법.
- 동시성 문제 해결: 데드락, 레이스 컨디션 등의 문제와 해결 방법.
- 비동기 프로그래밍: 비동기 함수와 이벤트 루프(예: 콜백, 프라미스, async/await).
- 병렬 처리: 병렬로 작업을 나누어 실행하는 방법 (예: 맵리듀스, 멀티프로세싱 라이브러리).
공부 순서의 예시
- 기본 개념 이해: 스레드와 프로세스의 차이, 동시성의 필요성.
- 스레드와 프로세스 사용법: 예제 코드 작성.
- 동기화 기법: 뮤텍스, 세마포어 등 사용해보기.
- 비동기 프로그래밍: 비동기 코드 작성 연습.
- 고급 주제: 병렬 처리 기법과 실제 사례 학습.
이런 기초 지식들을 차근차근 학습하면 동시성 처리에 대한 이해가 깊어질 것입니다.
혼돈하지말 것 스레드가 작은 개념임.
스레드와 프로세스의 기본 개념과 차이점은 다음과 같습니다:
- 프로세스: 운영체제에서 실행 중인 하나의 독립된 프로그램. 각 프로세스는 독립된 메모리 공간을 가집니다.
- 스레드: 프로세스 내에서 실행되는 작업의 단위. 여러 스레드가 하나의 프로세스 내에서 메모리 공간을 공유합니다.
비유: 프로세스가 하나의 프로그램이라면, 스레드는 그 프로그램 내에서 실행되는 여러 작업이라고 할 수 있습니다.
따라서, 스레드가 더 작은 개념입니다. 프로세스는 독립적이고 스레드보다 큰 개념입니다.
동시성의 필요성
동시성은 시스템의 자원을 효율적으로 활용하고, 사용자 경험을 개선하며, 처리 속도를 높이기 위해 필요합니다.
간단한 스레드와 프로세스 사용법을 보여주는 예제 코드
public class SimpleThread extends Thread {
@Override
public void run() {
System.out.println("스레드가 실행 중입니다.");
}
public static void main(String[] args) {
SimpleThread thread = new SimpleThread();
thread.start(); // 스레드 시작
}
}
Java에서 스레드를 start() 메소드로 시작하면, 해당 스레드는 자동으로 실행되며, 스레드가 종료될 때까지 기다릴 필요는 없습니다. 하지만 스레드를 명시적으로 종료할 수는 없습니다.
스레드는 run() 메소드의 실행이 끝나면 자연스럽게 종료됩니다. 만약 스레드가 특정 조건에서 중지되길 원한다면, 안전하게 종료할 수 있도록 플래그를 사용하는 방식이 일반적입니다.
public class MyThread extends Thread {
private volatile boolean running = true;
@Override
public void run() {
while (running) {
// 작업 수행
}
}
public void stopThread() {
running = false; // 스레드 종료 신호
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 스레드 시작
// 다른 작업 수행...
thread.stopThread(); // 스레드 종료 요청
}
}
요약
start()로 스레드를 시작.
스레드는 run()이 끝나면 종료.
직접 종료하고 싶으면 플래그를 사용.
run() 메소드를 @Override 어노테이션을 사용하여 오버라이드하는 것입니다.
설명:
Thread 클래스를 상속받아 run() 메소드를 재정의(오버라이드)함으로써 스레드가 실행할 작업을 정의합니다.
while (running) 루프 안에서 원하는 작업을 수행하도록 구현할 수 있습니다.
즉, 이 부분은 스레드가 시작되었을 때 실행할 코드를 작성하는 곳입니다.
KLOC (Kilo Lines Of Code)
코드 줄 수를 의미하며 주로 개발하는 프로그램이 얼마나 큰지를 나타내는 지표로 사용한다.
액티비티 다이어그램을 사용하면 코드 실행 흐름을 이해하는데 도움이 된다.
클래스 다이어그램은 구성 요소 간 정적 구조를 이해하는데 도움이 된다.
응집도(Cohesion)
응집도는 역할 또는 책임과 관련있다. 구성 요소가 역할에 따라 분리될수록 소프트웨어를 수정해야할 때 변경범위가 좁아진다.
캡슐화와 응집도
객체 지향에서 캡슐화encapsulotion는 데이터와 함수를 한곳에 모은다. 데이터를 외부에 노출하지 않고 감추는 정보 은닉information hiding과 캡슐화를 구분해서 표현할 수도 있지만, 흔히 말하는 캡슐화는 정보 은닉을 포함한다.
캡슐화는 응집도를 높이는 방법의 하나다. 객체 지향에서 캡슐화는 데이터에 대한 직접 접근을 최소화해서 구현을 감추고 외부에 기능을 제공한다. 이렇게 하면 구현을 변경해야 할 때 수정 범위가 캡슐화한 객체로 좁혀진다.
간단하게 캡슐화한 코드와 그렇지 않은 코드의 차이를 살펴보자. 먼저 캡슐화 하지 않은 코드를 먼저 보자. 아래 코드는 캡슐화를 설명할 때 자주 사용하는 예시이다.
public class Member {
private LocalDate expiry;
...
getter 생략
}
위 클래스를 사용하는 코드는 회원이 만료됐는지 판단하기 위해 다음 코드를 사용한다.
Member m = getMember (id) ;
if (m.getExpiry() .isBefore (LocalDate.now()) {
}
다른 곳에서 만료 여부 판단이 필요하면 이 코드를 그대로 복사해서 사용하게된다.
그런데 만료일을 무한대로 제공해야 할 필요성이 생겨서 expiry 값을 null로 저장하기로 했다고 가정하자. 이제 getExpity()를 사용한 코드를 모두 찾아서 다음과 같이 변경해야 한다.
if (m.getExpiry() != null && m. getExpiry() . isBefore (LocalDate. now.)) {
}
캡슐화한 코드를 보자.
public class Member {
private LocalDate expiry;
public boolean isExpired() {
return expiry. isBefore(LocalDate.now));
}
캡슐화한 코드는 데이터(만료일)와 관련 로직(만료 여부 판단)을 한 클래스에 모았다. 외부에는 만료 여부를 판단해서 알려주는 기능인 isExpired()만 노출하고 expiry는 제공하지 않는다. 만료 여부에 따른 분기 처리를 하려 할 때 Member 클래스를 사용하는 코드는 Member의 expiry에 접근하지 못한다.
대신 Member가 제공하는 isExpired()를 사용해서 분기 처리한다.
if (m. isExpired()) {
}
요구 사항 변경으로 만료일이 없을 때 null을 사용하기로 했다고 하자. 이때 Member 클래스는 다음과 같이 바뀐다.
public class Member {
private LocalDat expiry;
public boolean isExpired() {
return expiry != null && expiry. isBefore (LocalDate.now.));
}
반면 Member를 사용하는 코드는 다음처럼 바뀌지 않는다.
//isExpired()만 바꿀 뿐, isExpired()를 사용하는 코드는 바뀌지 않음
if (m. isExpired()) {
}
캡슐화하지 않은 코드에서는 만료 여부를 판단하는 로직을 변경할 때 여러 곳을 수정해야 하지만 캡슐화한 코드는 isExpired() 메서드 한 곳만 수정하면 된다. 관련 코드(데이터와 데이터를 사용하는 로직)가 한곳에 모여 있어(즉, 응집되어 있어) 수정할 범위가 줄어든 것이다.
응집도를 높이고 결합도를 낮추려면 구성요소 간 상호작용을 최소화해야한다.
구성 요소 간 상호작용을 최소화하려면 구현에 대한 의존을 줄이는 것이 중요하다.
캡슐화는 구현을 감춤으로써 두 구성요소 간 상호작용을 줄여주어 응집도를 높이는 동시에 결합도를 낮춰준다.
이벤트와 결합도
결합도를 낮추는 또 다른 방법은 이벤트를 사용하는 것이다.
회원가입을 다시 예로 살펴보자.
public class MemberRegister {
public void register (RegisterRequest req) {
validate (reg);
Member m = createPendingMember (req);
saveMember (m);
Events. raise(new MemberRegisteredEvent(m)); // 이벤트 발생
}
이 코드를 보면 Member를 저장한 뒤에 MemberRegisteredEvent가 발생하 고 있다. 회원에게 SMS로 통지하는 코드는 없다. SMS로 통지하는 코드는 이 벤트를 수신하는 리스너에 위치한다.
public class MemberEventListener {
public void handle (MemberRegisteredEvent event) {
..SMS 전송 코드 위치
}
}
바뀐 구조는 [그림 5-5]와 같다.
상속보다는 조립
상속을 생각하기 전에 조립을 먼저 고려하려는 원칙이다. 상속은 두 요소(상위 클래스와 하위 클래스) 간 강한 결합을 발생시키는데 조립을 사용하면 상속에 비해 결합도를 낮출 수 있다.
열거 타입을 사용하면 관련 상수를 한곳에 모을 수도 있다.
public enum BoilerType {
GAS (19), INDUSTRIAL (20) ;
private int code;
BoilerType (int code) {
this.code = code;
}
public int code() {
return code;
}
문자열로 된 매직 넘버(문자열)도 동일하게 상수나 열거 타입을 사용해서 이름 을 부여하는 방법으로 특정 값의 의미를 드러낼 수 있다. 의미가 드러나는 이름을 사용하면 코드 가독성이 높아지고 분석 시간이 줄어든다.
이름에 코드 값 함께 쓰기
매직 넘버를 상수나 열거 타입으로 바꿔서 이름을 부여하면 의미가 잘 드러난다. 그런데 코드를 분석하다 보면 "그래서 GAS의 실제 값은 뭐지?"라는 생각이 들 때가 많다. 그럴 땐 선언 위치로 이동해서 값을 확인한다. 개발 도구에 따라 이름에 마우스 커서를 갖다 대 면 선언 내용을 보여주기도 한다. 하지만 나는 이름의 실제 값을 보기 위해 추가 행위를 하는 게 귀찮았다. 그래서 다음과 같이 이름 뒤에 실제 값을 포함해서 코드를 작성하기도 했다.
public enum BoilerType {
GAS_19(19), INDUSTRIAL_20(20);
...
}
테스트 커버리지에 집착하지 말기
개인적인 경험으로 볼 때 테스트 커버리지는 70~80% 수준이면 적당하다. 테스트 커버 리지가 높을수록 좋지만 90% 이상을 목표로 하면 만들지 않아도 될 테스트 코드를 작성 하는 일이 벌어지기 때문이다. 데이터를 담고 있는 Member 클래스의 getLastU pdate()
메서드를 테스트하는 코드를 만드는 식으로 말이다. 이런 테스트는 커버리지를 높일 수는 있지만 실제로 테스트 코드를 작성해서 얻을 수 있는 가치가 없다. 테스트 커버리지를 높 이기 위해 가치 없는 코드를 만드느라 시간 낭비하지 말자.
자동화된 테스트가 있으면 회귀 테스트를 쉽게 진행할 수 있다. 소프트웨어를 수정했을 때 수정한 기능 외에 다른 기능에 영향이 없는지 검증하는 테스트를 회귀 테스트regression test라고 하는데, 자동화된 테스트 자체를 회귀 테스트로 사 용할 수 있기 때문에 테스트 효율성을 높일 수 있다. 자동화된 테스트가 있다 면 QA 담당자는 신규 기능과 몇 가지 필수 기능만 검증하면 된다. 자동화된 테 스트가 많은 범위를 검증하기 때문이다.
개인적으로 자동화된 테스트 덕에 심각한 오류를 사전에 발견해서 고친 경험 이 있다. 문제가 될 뻔한 시스템은 신규 주문 시스템이었다. 새로운 주문 시스 템의 QA가 한창 진행 중일 때 시스템 인수인계를 진행했다. 인수인계하면서 몇 가지 활동에 집중했는데 그중 하나가 통합 테스트 코드 작성이었다.
주문생성, 결제 처리, 취소, 환불 등 중요하면서도 민감한 기능 위주로 통합 테스트 코드를 작성했다. 테스트 코드를 작성하면서 업무에 대한 이해도 함께 높였다.
새로운 주문 시스템의 시범 서비스를 시작하면서 통합 테스트를 수행하는 작업을 젠킨스에 등록했다. 매일 정해진 시간에 통합 테스트를 실행했고 전체 테스트를 통과하지 못하면 푸시로 알림을 보내도록 설정했다. 문제를 발견한 그 날도 정해진 시간에 통합 테스트 작업이 실행됐고 통합 테스트를 통과하지 못 했다는 푸시 알림이 왔다.
그때 통과하지 못한 통합 테스트는 정기 결제 기능 시나리오였다. 정기 결제와 미납 결제 건에 대해 카드 승인을 시도하는 기능이었는데 결제 대상자에 대한 결제를 시도하지 않으면서 테스트가 깨져버렸다. 고객이 지정한 결제일에 카 드 승인이 이뤄지지 않으면 항의하는 고객이 많았을 뿐 아니라 승인을 수작업 으로 재시도해야 하는 번거로움이 많은 기능이었다.
정기 결제 기능 테스트가 실패한 원인을 살펴봤는데 놀랍게도 주문 목록 조회 기능 때문이었다. 주문 목록을 조회하는 기능에 변경 사항이 있어 코드를 수정 했는데 상관없어 보이는 정기 결제 기능에 문제가 생긴 것이다. 원인은 주문 목록 조회 기능과 정기 결제 기능이 같은 코드를 사용한다는 데 있었다. 주문 목록 조회 기능을 수정하는 사람이 이 사실을 몰랐고 요구에 맞춰 코드를 변경 하다가 정기 결제 기능에 영향을 준 것이다.
만약 자동화된 테스트가 없었다면 정기 결제 기능에 문제가 있다는 사실을 모르고 운영에 반영했을지도 모른다. 비록 Q를 담당하는 인력이 있었지만 QA 는 주로 주문 생성, 취소, 수동 결제 같은 기능을 검증하는 데 초점을 맞추고 있었기에 시스템이 배치 batch 형태로 실행하는 정기 결제 기능은 검증하지 못했다. 자동화된 테스트로 심각한 오류를 발견할 수 있었고 그 덕분에 운영 환경 에 오류 코드가 배포되는 상황을 미연에 방지할 수 있었다.
반대 경험도 있다. 테스트 코드가 있었음에도 급한 마음에 테스트를 실행하지 않고 배포를 진행한 적이 있다. 아주 사소한 코드 변경이었기에 테스트를 실행 하지 않아도 된다고 생각했다. 하지만 착각이었다. 사소한 변경이었는데 마침 오타를 냈다. 그 오타는 데이터베이스 테이블 칼럼 이름이었고 시스템은 쿼리 실행 오류를 내기 시작했다. 만약 테스트를 실행했다면 테스트가 실패하면서 배포가 중단됐을 것이다. 또한 정신없이 로그를 뒤지고 급하게 코드를 고쳐서 배포하는 긴박한 상황도 겪지 않았을 것이다.
개발자는 항상 압박 속에 산다. 코드를 잘못 만지면 심각한 문제가 발생할 수도 있기 때문이다. 민감한 기능일수록 코드를 수정하는 데 부담이 커진다. 이런 부담을 줄일 방법의 하나가 테스트 작성이다. 테스트 코드가 검증하는 범위가 넓어질수록 내가 만든 코드가 문제를 일으키지 않는다는 확신이 커지고 코드 를 수정할 때 안정감을 느끼게 된다. 코드 변경에 대한 스트레스도 감소한다.
젠킨스:
젠킨스는 지속적인 통합(Continuous Integration) 및 지속적인 배포(Continuous Deployment, CD)를 자동화하는 오픈 소스 도구입니다. 젠킨스를 사용하면 코드 변경 사항을 자동으로 빌드하고 테스트하며, 테스트를 통과하지 못하면 알림을 보내는 등의 작업을 자동화할 수 있습니다. 이를 통해 개발 과정에서 발생할 수 있는 문제를 신속하게 파악하고 해결할 수 있습니다
"배치(batch) 형태로 실행된다"
"배치(batch) 형태로 실행된다"는 것은 작업이 실시간으로 처리되는 것이 아니라, 일정한 간격으로 한꺼번에 처리되는 방식을 의미합니다. 예를 들어, 정기 결제 기능이 배치 형태로 실행된다는 것은 특정 시간(예: 매일 자정)에 여러 결제 작업이 한꺼번에 실행된다는 뜻입니다. 이는 실시간으로 한 건씩 처리되는 방식과는 다르게, 일정 주기마다 모아놓은 작업들을 한꺼번에 처리하는 방식입니다.
배치 처리는 주로 대량의 데이터를 처리하거나, 정해진 시간에 주기적으로 수행해야 하는 작업에 사용됩니다. 예를 들어, 야간에 대량의 보고서를 생성하거나, 하루 동안 쌓인 데이터를 정리하는 작업 등이 배치 처리의 예입니다.
개발 시간은 코드를 작성하는 시간, 테스트하는 시간, 디버깅하는 시간으로 구 성된다. 개발을 완료할 때까지 개발, 테스트, 디버깅을 반복하게 된다. 개발 시 간을 줄이려면 코딩하는 시간뿐 아니라 테스트 시간과 디버깅 시간을 줄여야 하는데 TDD를 적용하면 테스트 시간과 디버깅 시간을 줄일 수 있다. 테스트 시간을 줄이려면 테스트를 수동으로 하지 말고 자동화해야 한다. 테스 트를 자동화하려면 테스트를 코드 형태로 만들어 실행해야 한다. 다시 말해 티 스트 시간을 줄이려면 테스트 코드를 만들어야 한다.
TDD는 처음부터 테스트 코드를 만들기 때문에 반복되는 테스트 시간을 줄여준다. 테스트 코드를 작성 하면 처음에는 개발 시간이 늘어나는 것처럼 느껴지지만 시간이 갈수록 반복되 는 테스트 시간을 줄여줘서 오히려 개발 시간이 줄어든다는 것을 알 수 있다. 코드를 작성한 시점과 테스트 시점 간의 차이가 벌어질수록 문제가 발생했을 때 원인을 찾는 데 더 긴 시간이 걸린다. 코드를 다시 읽고 분석해야 하기 때문 이다. 반면에 TDD는 기능을 구현하자마자 테스트를 실행한다. 테스트 직전 에 코드를 작성했기 때문에 테스트가 실패해도 원인을 빨리 찾을 수 있다. 그 만큼 디버깅 시간이 줄어든다.
TDD는 여러모로 개발시간을 줄여준다고함
TDD: TDD(Test-Driven Development)는 테스트 코드를 먼저 작성하고 그 테스트를 통과하는 기능을 구현하는 소프트웨어 개발 방법입니다.
아키텍처 고민하기
개발하다 보면 아키텍처라는 말을 자주 접한다. 아키텍처에 대한 내용을 말하 기에 앞서 아키텍처의 정의부터 살펴보자. 위키피디아에서는 아키텍처를 다 음과 같이 정의하고 있다.
소프트웨어 아키텍처는 소프트웨어 시스템의 추상적인 구조다. 각 구조는 소프트 웨어 요소, 요소 간의 관계, 요소와 관계의 속성(프로퍼티)으로 구성된다.
마이크로서비스는 아키텍처의 한 종류이며, MVC(모델-뷰-컨트롤러)나 MVP(모델-뷰-프리젠터) 또한 아키텍처이다. 차이점이라면 마이크로서비스는 시스템 수준의 아키텍처이고 MVC, MVP는 클래스 수준의 아키텍처다.
어디까지가 아키텍처이고 어디서부터가 상세 설계에 속하는지 명확하게 구분 하기는 어렵지만 확실한 것은 아키텍처가 설계 과정에서 나오는 결과물이라는 것이다.
보안 사고 사례
개발자는 성능이나 확장성과 같은 품질 속성에 매료되기 마련이다. 대규모 트래픽을 처리 하는 시스템을 구축하려면 여러 기법을 활용해야 할 때가 많고 이 과정에서 다양한 기술 을 경험할 수 있기 때문이다. 또한 대규모 트래픽을 처리할 수 있는 시스템을 만들었다는 자부심도 얻을 수 있다.
반면에 보안과 관련된 품질 속성은 겉으로 잘 드러나지 않기에 등한시하기 쉽다. D사에 서도 이런 일이 있었다. D사는 디지털 상품을 고객에게 판매하는 서비스를 제공했다. 트래픽이 높았고 매출도 많았다. 이상 증상을 발견한 건 매출 정산 시점이었다. 판매 건수에 비해 결제 금액이 맞지 않는 상품이 발견됐다.
조사 과정에서 원래 상품 가격이 아닌 0원에 판매된 거래가 있다는 사실을 알게 됐다.
500원에 구매해야 하는 상품을 0원에 구매한 내역이 발견된 것이다. 이렇게 0원에 판매 된 상품이 1~2건이 아니었다.
이런 문제가 발생한 원인은 단순했다. 결제 금액을 처리하는 과정에 문제가 있었다. 이 서 비스는 최종 구매 금액을 숨겨진 폼에 기록한 다음에 결제 프로세스를 진행했다. 결제를 진행하는 과정에서 금액을 재확인하는 절차가 없었다. 사용자는 브라우저 디버깅 도구를 사용해서 숨겨진 폼의 금액을 0으로 바꾸고 결제를 진행했고 그대로 구매하는 데 성공했 다. 보안 취약점을 발견한 사용자는 이런 식으로 여러 상품을 구매했다.
이와 유사한 사례는 K사에도 있었다. 문제가 된 기능은 '내 정보 조회' 기능이었다. 이 기 능은 로그인한 사용자 이름, 이메일, 주소 등 민감한 개인 정보를 제공했다. 로그인하지 않 은 사용자는 이 기능에 접근할 수 없었다. 그런데 정보를 보여줄 대상 아이디를 HTTP 요 청 파라미터로 받았다. 즉 파라미터값만 바꾸기만 하면 손쉽게 남의 정보에 접근할 수 있 있다, 실제로 이 기능이 문제가 되어 개인 정보가 유출되는 보안 사고가 발생했다.
품질속성은 비용과도 연결된다.
가용성을 높이는 방법 중 하나는 구성 요소를 이중화하는 것이다.
서버와 데이터 저장소를 이중화하면 가용성이 높아지는데 그만큼 장비 사용 비용도 증가한다.
주요 품질 속성
가용성(Availability)
시스템이 얼마나 오랫동안 사용할 수 있는지를 나타내는 속성이다.
99.99%와 같이 사용 가능 시간/전체 시간 비율로 표시한다.
성능(Performance)
시스템의 최대 처리량, 평균 응답 시간 등을 포함하는 속성이다.
확장성(Scalability)
자원을 추가해서 증가한 사용자나 트래픽을 처리할 수 있는 시스템의 속성이다.
탄력성(Elasticity)
필요에 따라 자원을 추가하거나 반환하는 능력이다.
견고성(Robustness)
실행 중에 발생하는 에러나 잘못된 입력을 다루는 능력이다.
결함 허용(Fault tolerance)
일부 기능에 장애가 발생해도 시스템이 운영을 지속할 수 있는 능력이다.
신뢰성/안전성(Reliability/safety)
시스템 고장에 대비한 안전장치가 필요한지 또는 생명에 영향을 주 는 중요한 시스템인지를 나타내는 속성이다.
유지보수성(Maintainability)
얼마나 쉽게 시스템을 변경하고 향상할 수 있는지를 나타내는 속성이다.
지역화(Localization)
다양한 언어에 대한 지원을 표현하는 속성이다.
테스트 가능성(Testability)
소프트웨어 결과물이 주어진 테스트 환경에서 얼마나 테스트할 수 있는지를 나타낸다.
합법성(Legal)
시스템이 지켜야 할 법적 규제나 요건을 나타낸다.
보안(Security)
데이터베이스에 암호화해서 저장해야 할 데이터, 통신 구간의 암호화 등을 나타내는 속성이다.
배포 가능성(Deployablity)
개발한 결과물을 제품에 쉽게 반영할 수 있는 정도를 표현한다.
추적성(Traceability)
무언가를 추적할 수 있는 능력이다.
마이크로서비스로 개발했어요
한 서비스의 개발 책임자 A 씨를 만난 적이 있다. A 씨는 품질 문제로 고민하고 있었다.
서비스 출시 목표 일정을 코앞에 두고 진행한 시연에서 오류가 발생한 적도 있었다고 한 다.A 씨와 아키텍처에 대한 대화를 나눴는데 대화 도중에 A 씨는 "모두 마이크로서비스 로 개발했어요"라고 말했다. 서비스는 20개 정도 된다고 했다. 트래픽 증가에 따른 확장 처리도 되어 있다고 했다.
마이크로서비스가 20개나 된다는 말에 다소 의아했다. A 씨가 책임지고 있는 개발 조직 은 백엔드 개발자가 5명, 프런트엔드 • 모바일 개발자가 5명이었다. 새로 만드는 서비스에 가까웠기 때문에 서버 확장을 고민할 만큼의 고객도 없었다. 그런데 서비스를 무려 20개 로 분리했다. 일부 서비스에 장애가 발생했을 때 대응할 수 있는 기법(서킷 브레이커 등)
도 적용되어 있지 않았다. 그야말로 서비스만 잔뜩 쪼갰다.
시스템 복잡도만 높아지고 이득은 없는 결정을 한 개발 책임자를 전문가라고 할 수 있을까?
A 씨는 아마추어 같은 결정을 내렸다. 이런 식으로 특정 아키텍처를 적용해서 자아실
•원을 해서는 안 된다. 본인이 하고 싶은 게 아닌 상황에 맞는 적절한 아키텍처를 만들어야
한다는 것을 잊지 말자.
비동기 연동을 도입하면 시스템간 독립성이 높아진다.
본질적 복잡성과 우발적 복잡성
몇 해 전 개발 업계의 지인한테서 비동기 사용 사례를 들은 적이 있다. 지인의 업체는 모 바일 앱과 서버 간 통신을 2단계로 나눠서 처리했다. 모바일 앱이 서버 AP를 호출하면 서버는 그 요청을 큐Queue에 저장하고 바로 응답한다. 이후 모바일 앱은 처리 상태를 확인 하기 위해 주기적으로 상태 확인 API를 호출한다. 서버가 클라이언트 요청을 비동기로 처리함으로써 트래픽 부하에 대응할 수 있도록 했다.
문제는 서버에 그 정도 부하가 발생할 일이 없다는 데 있었다. CTO 역할을 맡은 개발 담 당자는 현실을 고려하지 않고 자신이 이전에 좋다고 생각한 기술과 구조를 도입했다. 그 결과 서버 구조가 복잡해지고 모바일 앱과 서버 간 통신 방식도 복잡해졌다. 하지만 시스 템의 복잡함이 빚을 발할 순간은 오지 않았다.
이렇게 해결해야 할 문제와 상관없는 복잡함을 우발적 복잡성 ccidental complexity 이라고 부 른다. 기술 사용 욕구에 빠진 개발자에게 우발적 복잡성이 발생하는 경우를 종종볼 수 있 다. 더 쉽고 단순한 기술로 처리할 수 있는 문제를 불필요한 기술을 사용해서 복잡도를 높 이는 것이다.
반대로 해결해야 할 문제 자체가 복잡해서 생기는 복잡함을 본질적 복잡성essential complexiy이라고 한다. 예를 들어 높은 가용성과 높은 처리량을 함께 제공해야 하는 시스템은 그렇지 않은 시스템과 비교하면 복잡해질 수밖에 없다. 이렇게 문제 해결을 위해 필 수적으로 발생하는 복잡함이 본질적 복잡성이다.
개발자는 우발적 복잡성에 빠지지 않도록 경계해야 한다. 물론, 흥미로운 기술을 만나면 사용해 보고 싶고, 유명한 회사에서 썼다는 기술도 써 보고 싶기 마련이다. 심할 때는 어떤 기술을 쓰지 못하게 막는 상사한테 화가 날 때도 있다. 하지만 그럴수록 우발적 복잡성의 유혹에 빠지지는 않았는지 곱씹어보자. 당장 마주한 문제 또는 곧 닥칠 문제를 해결하기 위해 어쩔 수 없이 복잡성이 동반되어야 한다면 지속해서 설득하는 시간을 가져야 한다.
같은 문제를 해결하려 한다면 복잡한 구조보다 단순한 구조가 더 좋다는 사실을 항상 명심하자.
아키텍처 및 패턴
특정맥락에서 반복되는 문제해결법은 패턴이라고 부른다.
대표적인 몇가지 패턴
-아키텍처 패턴
-디자인 패턴
-기업통합 패턴
-결합 허용 패턴
아키텍처 패턴은 아키텍처 수준에서의 패턴을 말한다. 포트 . 어댑터Port And Adapter 아키텍처, 마이크로서비스 아키텍처, 이벤트 기반Event driven 아키텍처 등 이 아키텍처 패턴에 속한다. 각 아키텍처 패턴의 장단점이 다르므로 다양한 아키텍처 패턴을 알고 있으면 현재 상황에 맞는 아키텍처를 결정할 때 도움이 된다.
예를 들어 이벤트 기반 아키텍처를 사용하면 탄력성과 성능에는 장점이 있지만 트랜잭션 처리가 복잡해지고 테스트도 어려워진다. 요구하는 성능이 낮거나 규모가 작다면 계층 아키텍처를 기반으로 한 모놀리식 구조를 사용하는 게 나을 수 있다.
디자인 패턴에서는 GOF의 '디자인 패턴'이 유명하다. GOF 디자인 패턴은 전 략strategy 패턴, 커맨드command 패턴, 싱글톤singleton 패턴, 템플릿 메서드Template Method 패턴, 팩토리 메서드factory Method 패턴 등 개발 과정에서 자주 활용할 수 있는 여러 패턴이 있다. 나 역시 GoF 디자인 패턴에서 소개한 프록시proxy, 어댑 터 adapter, 전략 패턴을 자주 활용하고 있다.
기업 통합enterprise integration 패턴은 파일 전송부터 메시징에 이르기까지 시스템 간 통합을 위한 패턴을 말한다. 최근에는 기업 간 연동뿐 아니라 내부 시스템 간 연동도 증가하는 추세다. 아주 간단한 시스템도 독립적으로 존재하기보다 는 다른 시스템과 연동이 필요할 정도다. 이런 상황에서 기업 통합 패턴은 시스템 간 연동을 해결할 때 큰 도움이 된다.
처음 개발을 시작하면 당장 돌아가는 코드를 만드느라 에러 처리를 소홀히 하기 쉽다. 하지만 사소해 보이는 에리가 시스템에 큰 장애를 발생시킬 때도 있다. 에러 처리에 충분히 신경 쓰면 오류를 줄일 수 있지만 완전히 장애를 없앨 수는 없다. 제아무리 완벽하게 구현해도 어딘가에 구멍은 있기 마련이고, 설사 내가 만든 시스템이 완벽하더라도 연동하는 다른 시스템에 문제가 생길 때도 있다.
따라서 문제를 완전히 없애기보다는 문제가 생겼을 때 알맞게 대처하는 방법을 찾아야 하는데 이때 사용할 수 있는 패턴이 결함 허용Fault Tolerance패턴이다.
GoF는 Gangs of Four의 약자로 책의 저자가 네 명인 데서 비롯된 이름이다.
결함 허용 패턴은 에러 발견, 에러 복구, 에러 완화 등 어떻게 처리할지에 대 한 패턴을 포함하는 개념이다. 하트비트 Heortbeat, 재시작Restart, 재시도 제한Limit Retires, 서킷 브레이커Circuit Breaker 등이 결함 허용과 관련된 패턴이다.
마이크로서비스가 유행하면서 분산 시스템도 많이 증가하고 있다. 분산된 시 스템이 늘어나면 시스템 간 통합이 더 필요해진다. 또한 분산된 시스템이 많아 질수록 연동 과정에서 에러가 발생할 수 있는 가능성도 커진다. 이런 분산 시 스템을 관리하기 위해서 다양한 기업 통합 패턴과 결함 허용 패턴을 익힌다면 많은 도움이 된다.
패턴이 유용한 이유
패턴은 2가지 측면에서 유용하다. 첫 번째는 설계 시간을 단축해준다. 패턴은 맥락을 포함한다. 어떤 상황일 때 이런 패턴을 사용하라는 식이다. 개발하다 보면 여러 상황을 마주하게 되는데 다양한 패턴을 알고 있다면 문제가 생겼을 때 알맞은 해결책을 빨리 도출할 수 있다.
패턴이 유용한 두 번째 이유는 원활하게 소통할 수 있게 해준다는 것이다. 패턴은 전략 패턴, 애그리거트 Aggregate, 벌크 헤드bulkhead와 같은 이름을 갖고 있다.
상황, 구조, 동작 방식 등을 구구절절 설명할 필요 없이 이름만 말하면 모든 정보가 전달된다. 짧은 이름만으로도 다양한 정보가 전달되니 소통 효율이 높아 진다.
한 번에 모든 패턴을 학습할 수는 없지만 주기적으로 새로운 패턴을 익혀보자.
설계와 소통하는 데 큰 이점을 누리게 될 것이다.
전략 패턴 (Strategy Pattern): 실행 중에 알고리즘을 선택할 수 있도록 하여, 객체가 서로 다른 행동을 하도록 만드는 디자인 패턴입니다.
애그리거트 (Aggregate): 여러 개체를 모아서 하나의 개체로 표현하는 패턴으로, 주로 데이터 구조를 표현하거나 조작할 때 사용됩니다.
벌크헤드 (Bulkhead): 시스템의 부분적인 장애가 전체 시스템에 영향을 미치지 못하도록 격리시키는 디자인 패턴입니다.
벌크헤드 패턴을 구현하는 방법에는 여러 가지가 있지만, 주로 다음과 같은 방식으로 가능합니다:
- 리소스 격리: 서로 다른 리소스 풀을 사용하여 서비스 요청을 분리합니다. 예를 들어, 데이터베이스 연결 풀을 여러 개의 세부 그룹으로 나누어 각 그룹에 대한 요청을 격리시킵니다.
- 스레드 풀 분할: 서로 다른 서비스 또는 기능에 대해 별도의 스레드 풀을 할당하여 각각을 격리시킵니다. 이는 한 서비스의 과부하가 다른 서비스에 영향을 미치는 것을 방지합니다.
- 서비스 인스턴스화: 독립적인 서비스 인스턴스를 생성하여 각 서비스가 독립적으로 실행될 수 있도록 합니다. 이는 한 서비스의 오류가 다른 서비스에 전파되는 것을 막아줍니다.
이러한 방법들을 조합하여 각 구성 요소가 독립적으로 동작하도록 만들어 시스템 전체의 안정성을 높이는 것이 벌크헤드 패턴의 핵심입니다.
배달이면 배달 리소스풀, 회원가입 리소스풀이면 회원가입리소스풀 이렇게?
맞습니다, 그런 식으로 이해하시면 됩니다! 예를 들어,
배달 서비스: 배달 주문을 처리하는 데 사용되는 데이터베이스 연결들이 모여 있는 배달 리소스 풀이 있을 수 있습니다.
회원가입 서비스: 사용자의 회원가입 정보를 처리하는 데 사용되는 데이터베이스 연결들이 모여 있는 회원가입 리소스 풀이 따로 설정될 수 있습니다.
각각의 리소스 풀은 해당 서비스의 특정 기능을 처리하는 데 필요한 데이터베이스 연결을 관리하며, 이는 서로 다른 기능을 격리하여 서비스 간의 상호 작용을 최소화하고 시스템 전체의 안정성을 높이는 데 도움을 줍니다.
스레드풀을 공장에 비유해서 설명해볼게요:
공장 비유로 스레드풀 설명
공장:
작업을 처리하는 곳
여러 작업자(스레드)가 있음
작업자들이 각자 작업을 처리
스레드풀:
작업을 처리하기 위해 미리 준비된 스레드의 집합
공장에서의 작동 원리
작업이 들어옴:
주문(작업)이 공장에 도착
작업자 배정:
관리자가 작업을 할당
미리 준비된 작업자 중 한 명이 주문을 처리
작업자 부족:
만약 작업자가 모두 바쁘면, 새 작업이 대기열에 추가
작업자가 하나의 작업을 끝내면 대기열에서 새로운 작업을 가져와 처리
작업 완료:
작업자가 일을 마치면, 새로운 작업을 기다림
공장은 필요한 만큼 작업자를 미리 준비해 작업의 지연을 최소화
벌크헤드 패턴에서 스레드풀을 분할하는 것은, 서로 다른 서비스나 기능에 대해 독립적인 스레드풀을 할당하여 한 서비스의 과부하가 다른 서비스에 영향을 미치는 것을 방지하는 방법입니다. 이는 마치 배에 여러 격실을 만들어 한 격실이 침수되더라도 다른 격실은 영향을 받지 않도록 하는 원리와 같습니다.
아주 간단한 예제 코드를 통해 설명해드리겠습니다.
예제 설명
아래 예제는 두 개의 서비스 (ServiceA와 ServiceB)가 각각 독립적인 스레드풀을 사용하여 작업을 처리하는 방법을 보여줍니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BulkheadPatternExample {
// 두 개의 독립적인 스레드풀 생성
private static final ExecutorService serviceAPool = Executors.newFixedThreadPool(5);
private static final ExecutorService serviceBPool = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
// 서비스 A 작업 실행
for (int i = 0; i < 10; i++) {
serviceAPool.execute(() -> {
try {
System.out.println("Service A is processing a task.");
Thread.sleep(1000); // 작업 처리 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 서비스 B 작업 실행
for (int i = 0; i < 10; i++) {
serviceBPool.execute(() -> {
try {
System.out.println("Service B is processing a task.");
Thread.sleep(1000); // 작업 처리 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 스레드풀 종료
serviceAPool.shutdown();
serviceBPool.shutdown();
}
}
코드 설명
스레드풀 생성:
serviceAPool: ServiceA용 스레드풀 (고정된 5개의 스레드)
serviceBPool: ServiceB용 스레드풀 (고정된 5개의 스레드)
작업 할당:
ServiceA와 ServiceB의 각각에 10개의 작업을 할당
각 작업은 간단히 메시지를 출력하고 1초간 잠잠
스레드풀 종료:
모든 작업이 완료된 후 스레드풀을 종료
이 예제는 서비스 A와 서비스 B가 각각 독립적인 스레드풀을 사용하여 서로 간섭 없이 작업을 처리하는 방법을 보여줍니다. 실제 애플리케이션에서는 각 서비스에 대해 더 구체적인 작업을 수행하도록 스레드풀을 구성할 수 있습니다.
처음부터 끝까지
한의 일을 주변 처음부터 끝까지 책임지는 것이 기본이다.
업무를 관리할 때 기초가 되는 것을 나열한 것이다.
-업무 나누기
-위험 관리
-요구 사항 이해 및 변경 대응
-일정 관리(또는 계획)
주니어개발자들에게 경험이 쌓이면 더 큰일이 주어진다.
작은 기능 단위로 주어지면 일이 작은 서비스를 만드는 수준으로 커진다.
더 일 잘하는 방법도 공부하기
C사에 다닐 때의 얘기다. 다닌 지 1년 정도 지난 시점에 팀장이 나한테 책을 한 권 줬다.
사회 초년생을 위한 '일 잘하는 0대리' 부류의 책이었다. 이 책에는 WBS, 임계 경로, 업무 계획 등 어떻게 일을 계획하고 진행할지에 대한 내용이 담겨 있었다. C사에 다니면서 이런저런 개발도 하고 구현 기술도 경험해봤지만 가장 도움이 된 건 이 책이었다.
일을 잘하려면 다양한 역량을 갖춰야 한다. 개발자가 개발을 잘하려면 능숙하게 구현 기 술을 다뤄야 한다. 구현 기술이 없으면 애초에 개발을 할 수 없기 때문이다. 하지만 개발 또한 일이기에 개발을 잘하려면 구현 기술 외에도 필요한 여러 역량을 함께 키워야 한다.
그런데 많은 개발자가 구현 기술 외에 다른 역량을 키우기 위한 학습에 소홀할 때가 많다.
구현 기술은 결과가 바로바로 나오기에 학습하는 과정이 재미있다. 반면에 일을 잘하는 법을 다루는 책은 읽는다고 해서 바로 결과가 나오지 않는다. 내용도 흥미롭지 않을 때가 많다. 하지만 개발 역시 일이기에 개발을 잘하려면 다른 역량도 함께 키워야 한다.
- WBS (Work Breakdown Structure): 프로젝트를 관리 가능한 작은 작업 단위로 분해한 구조.
- 임계경로 (Critical Path): 프로젝트 완료까지 걸리는 최장 경로로, 지연 시 전체 프로젝트 일정에 영향을 미치는 작업들의 연속.
위험관리
본인이 느끼기에 뭔가 잘 진행되지 않거나 모르는 게 있을 때 또는 명확하지 않은 점이 생겼다면 위험 신호라고 여겨야 한다. 위험 신호를 감지하면 빠르게 공유해야 한다. 어떻게 개발해야 할지 감을 못 잡고 있으면서 어떠해서든 혼자 해보겠다고 발버둥 치면 안 된다. 오히려 문제를 더 키울 뿐이다.
위험 공유 못지않게 위험 확인도 필요하다. 주니어 개발자 딱지를 떼면 이제 본인이 몇 명의 주니어 개발자와 함께 개발을 진행해야 한다. 즉 본인이 맡은 작업의 위험뿐 아니라 함께 일하는 주니어 개발자가 빠져 있는 위험도 확인할 필요가 있다. 본인이 주니어 시절에 겪었던 실수나 경험을 바탕으로 주니어 개 발자가 놓치기 쉬운 위험 요소를 확인하고 해소해야 한다.
위험 상황을 관리하기 위해 미리 위험 목록을 작성해보자. 위험 요소는 어떤 게 있는지 검토해보고 5개 이상 찾아서 목록을 만들어보자. 떠오르는 위험 요 소가 없다면 놓친 게 없는지 상급자와 논의하자.
위험 목록을 만들 때 등급을 함께 정리하면 더 좋다. 등급이 높을수록 일에 주 는 영향이 크니 신경 써서 관리해야 한다
'PROGRAMMING > 도서 내용 정리' 카테고리의 다른 글
파친코 책 정리 (0) | 2024.08.17 |
---|---|
The reader 책 정리 (0) | 2024.08.15 |
모비딕 책 정리 (0) | 2024.05.11 |
[비공개]오늘부터 개발자 도서 정리 (0) | 2024.03.22 |
개발자가 되기 위해 꼭 알아야하는 IT 용어 (0) | 2024.02.08 |