𝗪𝗵𝘆 𝗶𝘀 𝗠𝗮𝗽 𝗡𝗼𝘁 𝗜𝘁𝗲𝗿𝗮𝗯𝗹𝗲 𝗶𝗻 𝗝𝗮𝘃𝗮?— Most Asked Interv

“Why is Map not directly Iterable in Java?”

blog.stackademic.com

 

왜 Map은 반복 가능하지 않은가?

이 글에서는 Map의 반복 가능하지 않도록 설계된 이유에 대해서 말합니다.
이를 확장해 class, interface 상속에 대한 내용도 추가해 놓았습니다.


Java Collection 프레임워크의 구조를 보면 알 수 있듯이, MapCollection이 아니며 Iterable 또한 상속받지 않습니다.
(Iterable 을 상속의 의미는 Enhanced For Loop 를 사용할 수 있음을 말합니다)

위의 글에서는 왜 "MapIterable 하지 않은가?" 에 대해 말합니다.
주된 주장은 MapCollection, 즉 요소의 집합이 아니며 Pair의 묶음이라는 것 입니다.
Map은 기본적으로 key-value 형태의 쌍이기 때문에, 명시하지 않는다면 어떤 원소값의 반복인지 알 수 없습니다.

  1. key 값을 반복
  2. value를 반복
  3. key-value 쌍을 반복

이렇게 다양한 옵션들이 있기 때문에 정확히 무엇을 순회할건지를 알 수 없는 모호함이 발생한다는 것으로
정확히 어떤것을 순회할지를 나타낸 후에야 Iterable해지는 것을 알 수 있습니다.

Map<String, Integer> scores = new HashMap<>();
scores.put("Madhavi", 90);
scores.put("Kiran", 85);
scores.put("Anita", 88);

// Iterate over entrySet
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Iterate over keys
for (String key : scores.keySet()) {
    System.out.println("Key: " + key);
}
// Iterate over values
for (Integer value : scores.values()) {
    System.out.println("Value: " + value);
}

class를 여러개 상속 받을 수 없는 비슷한 이유

이것과 비슷한 개념으로 class의 상속이 존재하는데요
class는 기본적으로 상태를 가지게 됩니다.

상태를 가진다는 것은 Field 값이 존재한다는 것이고, method 를 통해 이를 조작하게 됩니다.
재밌는 상상을 해볼까요?

만약 dragon, bird 두 개의 클래스를 상속받는다 가정하고
각각이 fly() 라는 메서드와 ,wings 라는 필드가 존재한다고 하겠습니다.

class Bird{

    int wings;

    public void fly(){
        // do something
    }

}

class Dragon{
    int wings;

    public void fly(){
        // do something
    }
}

class bat extends Dragon, Bird{
    // 누구의 날개를 사용해서 어떻게 날 것인가?
    // error 
}

이를 상속받은 클래스에서는 과연 누구의 wings를 가지고 어떻게 fly() 연산을 수행해야 할까요?
이렇게 직접적인 구현을 가지고 있는 클래스는 여러개를 상속받게 하는 순간 모호함에서 오는 이상이 발생할 수 있기 때문에
자바에서는 원칙적으로 막아두는 것 입니다.

반면에 interface는 상태를 가지지않고, 그저 어떤 연산이 가능한지만 명시해놓는 명세서입니다.
따라서 여러개를 상속받아도 모호함에서 오는 이상이 없는 것 이죠

그렇기 때문에 interface의 경우는 여러개를 상속받는 것을 막아두지 않습니다.

 

 

Why Java Streams Have No Place in Production — Here’s the Brutal Truth Nobody Admits

Introduction:

medium.com

 

Stream 성능문제에 관한 글

본 글에서는 For loop 연산에서 Stream으로 변경하니 더 많은 메모리와, GC부담, 낮은 성능이 발생했다는 말을 합니다.

변경 전

    public List<TransactionSummary> processTransactions(List<Transaction> transactions) {
        List<TransactionSummary> summaries = new ArrayList<>();
        for (Transaction txn : transactions) {
            if (txn.getAmount() > 0) {
                double fee = calculateFee(txn);
                summaries.add(new TransactionSummary(txn.getId(), txn.getAmount(), fee));
            }
        }
        summaries.sort(Comparator.comparing(TransactionSummary::getAmount).reversed());
        return summaries;
    }

변경 후

    public List<TransactionSummary> processTransactions(List<Transaction> transactions) {
        return transactions.stream()
                .filter(transaction -> transaction.getAmount() > 0)
                .map(transaction -> {
                    double fee = calculateFee(transaction);
                    return new TransactionSummary(transaction.getId(), transaction.getAmount(), fee);
                })
                .sorted(Comparator.comparing(TransactionSummary::getAmount).reversed())
                .collect(Collectors.toList());
    }

느려진 이유로는 filter, map, sorted 연산을 거칠 때 마다 변환된 temp List 가 생기게되고
이로인해 연산에 더 많은 메모리와, 시간이 필요하다는 주장입니다.

그런데 조금 의문인 것이, 자바 패러다임 자체가 함수형으로 작성하는 방향으로 가고있으며
대규모 트래픽을 다루는 조직에서도 Stream을 적극적으로 사용하는 추세인데.. 더 느리다면 왜 사용할까요?

중간 연산 Vs 최종 연산

Stream은 중간 연산과 최종 연산의 개념이 존재합니다.

대부분의 중간 연산에서는 Lazy Evaluation이 발생하며, 이는 연산의 절차를 기록만 하다가 최종 연산(collect/forEach)등 을 만날때에 실제로 실행됨을 의미합니다.

따라서 연산 단계마다 변환된 temp List가 생기는 것이 아니라 한 요소에 대해서 filter, map 연산을 적용하고 이를 최종 연산에 전달하게 됩니다.

따라서 중간 연산을 거칠때마다 temp list가 발생해 느려졌다는 주장은 틀렸습니다.

그럼 왜 느려졌는가?

대부분의 중간 연산에서는 Lazy Evaluation 처리를 합니다.
즉, 모든 중간 연산이 지연 평가가 발생하는 것은 아닌데요

위에서 사용된 sorted 연산은 정렬하는 중간 연산입니다.
하지만 정렬의 특성상 앞서 나온 모든 요소가 필요하며, 실제로 map 연산을 거친 원소를 최종 연산에 바로 전달하는 것이 아닌
모든 원소에 map을 적용한 결과를 메모리에 쌓은 후 sorted하게 됩니다.

For loop 에서는 전체에 대해서 변환 작업을 실행한 후 sort를 시행하기 때문에 성능이 잘 나오는 것으로
Stream 마지막에 따로 sort 연산을 한다면 비슷한 성능이 나오게 됩니다.

굉장히 자주 사용하지만, 모호한 부분들이 많은 것 같아요.

 

We Used List Everywhere — And It Broke Our API Design

And What Happened When We Finally Refactored Ours

medium.com

 

개발을 하다보면 수많은 곳에서 List 형태를 사용합니다.

왜 그런가?

  1. 쓰기 쉽다
  2. 대부분 효과 있음
  3. Collection Framework 의 일환으로 다양한 기능 제공

일종의 기본 소양처럼 사용하곤 합니다.

또 List 라는 자료구조는.. 다음을 내포합니다

  1. 순서가 중요하다.
  2. 변경 가능하다.
  3. 인덱스 접근이 예상된다.
public void sendNotifications(List<User> users) {
    for (User u : users) {
        notificationService.sendEmail(u);
    }
}

하지만 순서가 필요하지 않다면?, 인덱스 접근이 필요 없고 단순 순회만 한다면?
리스트를 사용해야할 이유가 있을까?, 또한 저 코드는 인자로 들어온 것이 가변 리스트인지, 불변 리스트인지 구분할 수 없습니다.

이러한 이유 때문에 글에서는  List<T>를 사용하기 보다는Collection<T>Interface에 의존하거나
Iteratable 을 메서드 시그니처로 사용하자고 주장합니다.

public void sendNotifications(Collection<User> users);
public void sendNotifications(Iterable<User> users);

 

public void sendNotifications(Stream<User> users);

이런식으로 코드 작성이 가능하겠죠

개인적으로는 Iterable<T> 이 마음에 드는데요, 단순하게 순회만 할꺼야! 라는 의도가 굉장히 명시적이라 생각합니다. 
또 들어오는 인자에 대해서 수정을 가하는 행동이 없음으로 가변/불변 리스트등의 사용에서도 자유롭지않을까요? 

글에서는 다음과 같은 체크리스트를 통해서 List<T>의 사용 여부를 다룹니다.

When You Should Actually Use List

There are valid cases! Use List<T> when:

You need to access elements by index (list.get(3))
You care about the exact order of items
You plan to mutate the collection (add/remove/sort)
You explicitly want to signal that caller should pass a List
Otherwise? Don’t.

✅ Our Refactor Checklist
Here’s how we audited all our method signatures:

Does the method use .get(index)? → Keep List<T>
Does the method add/remove elements? → Keep List<T>
Only iterates or filters? → Switch to Collection<T> or Iterable<T>
Works best with lazy evaluation or large data? → Use Stream<T>

Collection & Iterable 

Image

최상위에 Iterable 존재, Collection 은 이를 상속

List,Set,QueueCollection을 상속받고 Iterable

Image

따라서... Map의 경우 에러가 나는 모습을 볼 수 있습니다.

이런식으로 Collection으로 지정한다면, 확실히 Set,Queue 등 다양한 자료구조가 하나의 메서드를 재사용할 수 있음으로 장점이 있고

호출하는 쪽에서도 별도의 자료구조 변환을 해주지 않아도 사용 가능하기 때문에 충분히 매력적인 선택지라고 생각됩니다.

 

 

The Results Shocked Me — I Tried Writing Java Like It’s 2025

Legacy who? I pushed Java to the future — and it pushed back.

medium.com

 

자바를 2025년 답게 쓰자는 글 입니다.

몇몇 공감가고, 도입예정인 부분들에 대해서만 코멘트 남기려 합니다.

✨ Switched to Virtual Threads — Everything Changed

Virtual Thread (이하 VT)에 대한 사용이 가장 먼저 나오는데, 기본적인 개념은 있지만 사용해 본적은 없어
Dawn-Cs-Study 프로젝트를 하면서 사용해볼까 합니다.

VT의 개념자체가 자바가 사용하는 OS Thread를 VT를 핸들링하는 Carrier Thread로 사용하여 
한정된 스레드 갯수로 더 많은 스레드를 태우겠다는 것으로 알고있습니다.

 

Self Study • masiljangajji

Self Study

github.com

아래는 VT 도입 이전, Thread, Executor 관련해 공부하면서 남긴 내용들입니다.

Thead 쓸 때마다 생성해서 쓰면 안되나요?

기존에는 Thread 객체를 직접 생성해서 start() 해야 했음

Thread thread = new Thread(() -> {
    System.out.println("Hello World!");
    });

thread.start();  // 새로운 스레드 생성 후 실행 
thread.run();  // 기존 요청을 물고온 스레드로 실행 (스레드 생성 X)

이 방법은 조금 머리아픈게.. new Thread() 할 때마다 스레드가 생성 됨
start() 실행이 끝나면 OS Thread는 즉시 반납되지만, 자바 Thread 객체는 힙에남아서 공간을 점유함

또 한 번 실행이 끝난 Thread 객체는 다시 start() 못하며, 몇 개의 스레드가 만들어졌는지 예상하기 어려움.
실행자체는 큰 문제가 없을 수 있으나, 관리의 측면에서 생산성이 매우 떨어진다.

Executor Vs Executors

Thread 실행 정책을 추상화 하여 만든 Interface
new Thread() 를 직접 다루지 않도록 하기 위해 도입됨

public interface Executor {
    void execute(Runnable command);
}

스레드 생성, 스케줄링 전략은 구현체에 위임합니다.
ThreadPoolExecutor, ScheduledThreadPoolExecutor 등이 대표적인 구현체

        return new ThreadPoolExecutor(
                core,
                max,
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(queueCapacity),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

그런데.. ThreadPoolExecutor는 생성자 파라미터가 너무 많아서 설정이 귀찮습니다.
그렇기 때문에 자주 쓰는 패턴을 간단히 팩토리 메서드로 제공하기 위해 Executors 유틸 클래스가 만들어집니다.

Executors.newFixedThreadPool(int nThreads);   // 고정 크기 스레드풀
Executors.newCachedThreadPool();              // 필요시 스레드 생성, 유휴 시 종료
Executors.newSingleThreadExecutor();          // 1개 스레드만 사용
Executors.newScheduledThreadPool(int core);   // 주기적 실행 지원

하지만.. 실무에서는 보통 세부적인 커스텀을 하기 때문에 ThreadPoolExecutor 만들어 쓰는 경우가 더 많습니다.

여기서 한발자국 더 나아가면 이제는 OS Thread 하나에 여러 VT를 태워서 쓰는 방식으로 갑니다.
더 적은 Thread로 더 많은 작업을 수행할 수 있겠죠?

🧬 Embraced Records + Sealed Classes

레코드를 사용하자! 너무나 맞는 말입니다.
보통 DTO를 다룰때 Record 사용하거나 VO 같은 녀석들을 다룰때 사용하지 않나 싶습니다.

개인적으로는 불변성을 지켜줘야 한다!, 얘는 불변해!, 이런 의미를 내포하고 싶을때도 사용합니다.

sealed 키워드는 사용은 안해봤는데요, 이번 기회에 알아봤습니다.

이 타입이 어떤 클래스/인터페이스가 상속할 수 있는지를 명시적으로 제한합니다.

public sealed interface Event 
    permits LoginEvent, LogoutEvent {}

public final class LoginEvent implements Event {
    // 더 확장 불가
}

public final class LogoutEvent implements Event {
    // 더 확장 불가
}

이런식으로 구현이 되어지는데, sealed의 구현체는 항상 final, sealed, non-sealed 중 하나여야 합니다.

언제 유용한가?

sealed는 하위 타입의 집합이 닫혀있음을 의미합니다.
따라서...

public sealed interface Event permits LoginEvent, LogoutEvent {}

public final class LoginEvent implements Event {}
public final class LogoutEvent implements Event {}

LoginEvent,LogoutEvent 외에 다른 이벤트가 나오면 안되는 경우, 컴파일 타임때 Event 를 구현하는 상황을 막을 수 있습니다.
DDD에서는 클래스들을 집합화 하고, 이를 Context로 묶는 경향이 있기 때문에 DDD를 도입한다면 명시적으로 나타내기 좋지 않을까? 생각도 듭니다.

또한 switch문과 결합시키는 패턴도 유용합니다.

sealed interface Event permits LoginEvent, LogoutEvent, SignUpEvent {}

record LoginEvent(String email) implements Event {}
record LogoutEvent(Long userId) implements Event {}
record SignUpEvent(String email, String nickname) implements Event {}

public class EventHandler {

    public String handle(Event e) {
        return switch (e) {
            case LoginEvent login   -> "로그인 처리: " + login.email();
            case LogoutEvent logout -> "로그아웃 처리: " + logout.userId();
            case SignUpEvent signUp -> "회원가입 처리: " + signUp.email();
        };
    }

    public static void main(String[] args) {
        EventHandler handler = new EventHandler();

        System.out.println(handler.handle(new LoginEvent("user@test.com")));
        System.out.println(handler.handle(new LogoutEvent(42L)));
        System.out.println(handler.handle(new SignUpEvent("new@test.com", "승재")));
    }
}

또한 Pattern Matching for switch문법은 JDK 17은 preview고 21부터 정식지원입니다.

들어가는 글

안녕하세요 오랜만에 글을 작성합니다.

블로그를 꾸준히 이어가겠다고 다짐했는데,
미디엄 기준 마지막 글 작성일이 5월, 티스토리 기준 2월인 것을 보면 반성하게도 되고
돌아보면 글을 쓸 시간조차 없을 만큼, 정말 가파르게 달려온 상반기였던 것 같습니다.

그만큼 많은 일들이 있었습니다.
우선 2018년에 입학한 학교를 2025년에 졸업하며 약 7년간의 대학 생활을 마무리했습니다.
또 최근에는 가비아 인턴십 전환에 실패하고, 카카오게임즈 최종 면접에서도 고배를 마셨습니다.

연달아 두 번의 실패를 겪고 나니 아쉽고 마음이 꺾이기도 했습니다.
특히 카카오게임즈 탈락 이후에는 며칠 동안 아무것도 하지 못한 채, 집에만 있었던 것 같아요.

그래서 나는 무엇을 배웠나?

실패는 늘 아프고 쓰라리지만 그만큼 크게 배웁니다.

가비아 인턴십 연계 실패에서는 "일을 대하는 자세를" 배웠고
카카오게임즈 최종 탈락에서는 "나를 온전히 이해하는 방법"을 배웠습니다.

가비아 인턴십을 진행하면서는 "나를 못난 사람으로 보면 어떡하지?" 라는 생각에 사로잡혀 있었습니다.

개발자로 어떻게 기여할 수 있을까 보다, 불안과 조바심이 앞섰죠.
결과적으로는 정직원 전환에 실패했고, 돌아보며 든 첫 생각은 "뭐가 그리 무서워서 안절부절 했을까?"였습니다.

어차피 결과는 컨트롤할 수 없는 일인데..
그 순간순간에 더 많은 걸 배우고, 얻고, 누려야 했다는 아쉬움이 남았습니다.

이번 카카오게임즈 최종 탈락 후에는 "그렇다고 생각하는 것과 실제로 그런 것은 다르다"를 깨달았습니다.
분명 내 이야기를 한 것 같은데, 그 안에 진짜 ‘나’는 없었던 것 같았어요.

잇단 두 번의 실패는 마치 두 다리가 부러지는 듯한 느낌이 듭니다.
하지만 동시에 이제는 정말 남의 평가가 아닌 스스로의 가치관과 생각을 말할 수 있다는 자신감이 생겼습니다.
다리가 부러지고 나서야 스스로 걷는 방법을 배우는 것 처럼요.

나는 왜 개발자가 되고 싶었나?

임원 면접에서 가장 어려웠던 질문이었습니다.
면접에서는 “해보니 재밌었다, 좋아하는 일은 더 잘할 수 있다고 믿는다”라고 대답했지만,
솔직히 말해 처음부터 개발을 좋아했던 건 아니었습니다.

대학교 전공도 큰 뜻이 있어서 선택한 건 아니었습니다.
남들이 다 가니까 그냥 무난하게 학교를 다녔고, 성적도 그저 그런, 평범한 학생이었죠.

그러다가 4학년 1학기가 됐을 때, 진로에 대한 고민을 하기 시작했습니다.
하지만 명쾌한 답은 없었고, 그 즈음 주목받기 시작한 개발자라는 직업을 선택하게 됐습니다.

하지만 그 순간 한 가지 다짐도 함께 했습니다.
그동안은 내가 큰 뜻이 없고 적당히, 무난하게만 살아왔으니 앞으로의 기간 동안은 정말 미친 듯이 달려보자
남들보다 뒤처진 만큼 더욱 몰입하고 밀도 있게 시간을 보내자고.

그렇게 NHN Academy에 지원해 서울에서 전라도 광주까지 내려가 10개월간 생활하며 Java라는 언어를 처음 접하고 Spring을 통해 웹 개발에 입문하게 됐습니다.

낯선 도시, 낯선 기술, 낯선 환경.
하지만 그 안에서 정말 몰입하며 누구보다도 열심히 배워왔다고 자부할 할 수 있습니다.

그 이후로는 블로그에 기록했듯, IT 커뮤니티 활동, 사이드 프로젝트, 해커톤, 오픈소스 기여까지
제 진심이 향하는 방향으로, 계속 배우고 나아가고 있습니다.

마무리하며

제 블로그에 방문해 주시는 분들은 대부분 개발자를 준비하시는 대학생분들이 많습니다.
여러분들은 왜 개발자가 되고 싶나요? 그 나름의 답을 찾아가실 수 있다면 좋겠습니다.

그 답이 처음부터 선명하지 않아도 괜찮고, 누군가의 말에 흔들릴 수도, 때론 실패에 무너질 수도 있습니다.
저도 그 과정을 지나오고 있고, 여전히 배우고 있는 중입니다.

하지만 분명한 건, 자기 속도와 방향으로 꾸준히 걸어가는 사람은 반드시 성장한다는 것입니다.
저 역시 제가 지나온 길을 공유하며, 조금이라도 그 여정에 도움이 되는 개발자가 되고 싶습니다.

앞으로도 함께 고민하고, 기록하고, 나누는 사람이 되겠습니다.
읽어주셔서 감사합니다.

들어가는 글

안녕하세요 최근 정말 좋은 일이 있었는데요 백엔드 포지션으로 가비아에 합류하게 됐습니다.
이번글은 합류의 과정과 제 개인적인 생각을 작성하려 합니다.

서비스 회사에 가고싶다

블로그에 따로 포스팅하거나, 이력서에 기재하지는 않았지만 SI 회사에서 인턴근무를 한 경험이 있습니다.
SI의 특성상 프로젝트의 생명주기가 짧으며 기한을 맞추는것이 핵심이기 때문에 일을 배우면서 성장하기에는 어려운 환경으로 느껴졌습니다.

생각했던것 보다 성장에 대한 욕심이 크다는걸 알게됐고 "나는 무조건 서비스회사에 가야겠구나" 다짐했던 기억이납니다.
이력서또한 SI/SM 서비스를 주로하는 기업에는 작성하지 않았으며 흥미를 느끼고 성장할 수 있겠다 생각되는 기업의 공고에만 작성했습니다.

작년에는 광탈 올해는 합격

가비아에는 작년 겨울에도 이력서를 접수했었는데요 서류에서 바로 탈락했던 기억이 납니다.
이때는 이력서를 작성하는 방법과 직무적합성을 나타내는 방법에 대한 이해가 많이 부족했습니다.

이력서란 기본적으로 나 자신을 세일즈 하는 것인데 그런 부분들이 들어나지 않았거든요
다만 표현의 방법을 바꾸고 강조하고 싶은 부분만을 보여줌으로써 다른 결과를 만든 것 같습니다.

동일한 프로젝트를 설명하는 이력서

첫 출근 후 느낀점

서로가 서로를 존중한다는 느낌을 받았습니다.
이 느낌이 굉장히 중요하다 생각하는데요, "돈 받으니까 일해야지" 당연히 맞는 말이지만 좋은 방식은 아니라 생각합니다.

"나의 일이 아닌 남의 일을 돈받았으니까 해준다" 라는 인식이 생기는 순간 동기부여가 되지 않습니다.
회사는 내것이 아니고 회사에서 처리하는 일또한 회사의 일이지만 마치 나의 일인것 처럼 생각하게 만드는 것이 좋은 회사의 덕목이라 생각하거든요

저는 자취를하며 월세를 살고있는데요, 이 집은 제 것이 아니기 때문에 방치하고 더럽게 살아도 아무런 문제가 없습니다.
하지만 대부분의 사람들이 마치 "나의 집" 처럼 청소도하고 꾸미기도 하며 내가 속해있는 환경을 나의 마음에 맞게끔 가꾸고 아껴주려고 노력합니다.

회사의 일도 비슷한 것 같습니다.
평생직장은 무의미해졌고 회사는 나의 소유가 아니지만, 그 울타리 안에서 일을 하는 동안에는 서로를 존중하며 열심히 일하는것이 서로에게 좋은 것 아닐까요?

많이 물어보시는 질문 - 개발자도 어학성적, 자격증 필요한가요?

서비스 회사는 없어도 되는 것 같습니다만, 보다 전통적인 산업의 기업 혹은 SI/SM 을 주로하는 경우에는 필요합니다.
개인적으로는 정보처리기사 정도만 필수로 취득하시면 될 것 같습니다.

마무리하며

제 블로그를 방문해주시는 대부분의 분들은 개발자 직무를 희망하거나 취업을 준비중이신 대학생 분들이 많은 것 같습니다.
특히나 NHN Academy , Univ 의 지원 시즌이 되면 조회수가 폭발적으로 나오는데 상당히 기쁩니다.

누군가에게 도움이 된다는 사실이 참 좋은 것 같아요, 감사합니다.

 

조금 늦은 2024년 회고

설 전까지는 2025 BETA 버전이기 때문에 정식 릴리즈된 지금 시점에 회고를 작성합니다.
2024년은 아쉬운 부분도, 만족하는 부분도 공존하지만 나름의 성과를 이뤄냈던 시간인 것 같습니다.

2024, 개발자스러운 것들

2024년은 새로운 도전의 연속이었습니다.

NHN Academy 프로젝트 과정 수료후 개발에 대해 자신감이 생겼고, 이를 토대로 어떤 활동이든 도전해보자! 라는 목표가 생기게 됐습니다.
특히나 개발자스러운 것에 굉장히 많은 도전을 했습니다.

 

2024년 , 성장을 만든 IT 활동 | Notion

SIPE 3기

sprinkle-place-c1a.notion.site


처음으로 컨퍼런스도 참석해 내심 부러웠던 스티커도 받아서 붙여보고… 해커톤을 수상하고, 발표도 하고, 오픈소스에 기여하는 등
좋은 개발자란 무엇이고 어떻게 하는 것이 가파른 성장을 이끌 수 있는진 모르겠으나 그냥 개발자스러운 것을 하면 그게 좋은 개발자 아닌가?라는 생각으로 달려온 것 같습니다.

한마디로 2024년은 개발자스러워지기 위한 과정이었습니다. 개발에 대해 아무것도 몰랐기 때문에, 역설적으로 정말 가파르게 성장할 수 있었습니다.

2025, 새로운 키워드 ‘꾸준함’

2025년은 뛰어난 개발자가 되는 과정이고 싶습니다. 개발자스러운 사람 말고… 그냥 뛰어난 개발자 말이죠.
그러기 위해서 가장 필요한 것은 꾸준함인 것 같습니다.

개인적으로 단기간에 몰입하고 성과를 내는것은 잘하지만 꾸준하게 이어나가는 뒷심은 부족하다 생각합니다.

깃허브 내역만 봐도.. 커밋을 많이 한 기간과 하지 않는 기간이 확연히 차이 나는 것처럼요

 

마무리하며

처음 블로그를 시작했을 때 아무도 봐주지 않아 친구들한테 링크를 보내면서 겨우겨우 조회수를 올렸던 기억이 납니다.

지금은 친구들이 방문해주지 않아도 꽤나 많은 분들이 방문해 주시는 것 같습니다.
어쩌면 첫 번째 꾸준함은 블로그였나 봅니다.

내가 작성하는 글들이 누군가에게 정보가되고 경험을 공유할 수 있다는 것이 정말 큰 힘이 됩니다.
모두 행복하세요

GPT 요약

My-Music-Note 프로젝트에서 AWS 기반 고가용성 아키텍처를 설계하고, Auto Scaling Group, Application Load Balancer, CodeDeploy를 활용해 검증을 진행했습니다.
성능 테스트 도구로 JMeter를 시도했으나 자원 소모와 불필요한 기능으로 Artillery로 전환하여 소규모 부하 테스트를 성공적으로 수행했습니다.
테스트 결과, 단일 EC2 환경에서는 부하 증가로 많은 실패가 발생했지만, My-Music-Note 아키텍처에서는 100% 성공과 낮은 레이턴시를 보여 고가용성을 입증했습니다.

My-Music-Note 프로젝트에서의 경험을 다룬 글입니다.

My-Music-Note 프로젝트에서 유일한 백엔드 개발자로서 AWS 기반 인프라 구축에 집중하였습니다.
이번 글에서는 제가 구현한 AWS 기반 고가용성 아키텍처를 검증하고 테스트한 과정을 공유하려 합니다.

고가용성 아키텍처 설계

이전 포스팅에서 고가용성 아키텍처를 설계할 때 고려했던 3가지 기준을 소개했습니다.

https://masiljangajji-coding.tistory.com/88

이 기준을 바탕으로 Auto Scaling Group(ASG), Application Load Balancer(ALB), 그리고 CodeDeploy를 사용하여 인프라를 구축했습니다. 그러나 설계하고 배포하는 것만으로 고가용성이 보장된다고 할 수는 없습니다.

따라서 실제로 고가용성을 충족하는지 검증이 필요했고 이를 위해 성능 테스트를 진행하기로 했습니다.

성능 테스트 툴 선택 - JMeter에서 Artillery로 전환

JMeter의 도입과 한계

처음에는 널리 알려진 성능 테스트 툴인 JMeter를 선택했습니다.
JMeter는 실무에서도 많이 사용되는 도구로 자바 개발자들 사이에서는 표준이 되는 툴이라 생각했기 때문입니다.

하지만 실제로 사용해보니 우리 서비스에는 적합하지 않다 판단했습니다.

  1. 무거운 자원소모
    • JMeter는 JVM 기반 특성상 상당한 메모리를 소모
  2. 대규모 부하 테스트의 불필요성
    • 테스트 환경의 EC2 인스턴스(t4g.nano)는 고성능 서버가 아니었기에 JMeter의 장점인 대규모 부하 테스트 또한 불필요
  3. 결과 시각화가 제한적
    • 분석이 필요한 경우 별도로 CSV를 내보내서 분석해야 함

켜놓기만해도 RAM을 엄청 잡아먹는다..

Artillery로의 전환

따라서.. 보다 가볍고 소규모 테스트에 적합한 대안을 찾아봤으며 최종적으로는 Artillery로 전환하게 됐습니다.

Artillery는 소규모 부하 테스트에 적합하며 자원 소모가 적은 경량 툴입니다. YAML 파일로 테스트 작성이 가능한데 이는 Spring Boot 프로젝트에서 properties를 관리할 때 사용하던 방식과 유사해 익숙하게 느껴졌습니다.

Locust와 K6 같은 다른 경량 테스트 툴도 검토했지만 각각 Python과 JavaScript로 테스트 스크립트를 작성해야 한다는 점에서 Artillery를 선택했습니다.

테스트 환경과 시나리오 설정

테스트 환경

테스트는 다음과 같은 AWS 리소스를 기반으로 진행되었습니다

  • EC2 : t4g.nano
  • RDS : db.t4g.micro

테스트 시나리오

사용자가 인덱스 페이지 방문 후 로그인하는 시나리오를 작성했으며 트래픽 부하에 따라 Auto Scaling Group과 Application Load Balancer가 요청을 적절히 분산시키는지를 확인하기 위해 5분간 점진적으로 요청을 증가시킨 후 1분간 유지하도록 설정했습니다.

동일한 시나리오에 대해서 target만 EC2 , ALB로 변경해 테스트

테스트 결과

단일 EC2 환경에서 테스트를 수행한 결과는 다음과 같았습니다.

Artillery Metric
요청이 증가함에 따라 time out 발생
AWS Metric , 요청이 증가함에 따라 Network I/O 증가

타임아웃 오류 : 전체 요청의 37.6%(14,766건)
작업 실패율 : 가상 사용자의 54.7%(14,766명)

시간이 갈수록 늘어나는 요청에 따라 부하가 걸리는 것을 볼 수 있습니다.

My-Music-Note 아키텍처에서의 결과

ASG, ALB, CodeDeploy로 구성된 My-Music-Note의 고가용성 아키텍처에서는 다음과 같은 결과를 얻었습니다.

Artillery Metric
99% 요청에 대해 레이턴시 25.8ms
AWS Metric , ASG에 의해 트래픽이 분산돼 Network I/O 하락

타임아웃 오류: 0%
작업 실패율: 0%
99% 요청에 대해 레이턴시 25.8ms

이 결과는 My-Music-Note 아키텍처가 실제로 고가용성 기준을 충족하고 있음을 입증했습니다.

마무리

이번 성능 테스트는 고가용성 아키텍처를 검증하는 데 성공적이었지만 매우 간단한 시나리오에 대해서만 테스트해 API 레벨에서의 병목은 확인할 수 없었습니다.

이 부분에 대해서 개선할 여지가 남아있으며 앞으로도 지속적으로 배움과 개선을 이어나가겠습니다.

같이 보시면 좋은 발표 영상입니다.

 

+ Recent posts