Spring Boot (+ RESTful)

RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (8)

wy-family 2025. 1. 10. 22:19

질문 16. 거의 마지막 즈음에 HashMap 을 만들어서, List<String> 을 put 을 해줘서 result 라는 걸 만들어줬어. 근데 여기서 이해가 안 되는게, HashMap 을 그대로 return 할 때 사용하면 되는데, 왜 굳이 List<HashMap> 을 만들어서 response 를 return 할 때 사용했을까? 왜 그런거야?

 

아주 좋은 질문이에요! 😊
질문을 보니 **"왜 굳이 HashMap을 바로 반환하지 않고, List<HashMap>으로 감싸서 반환했을까?"**가 궁금하신 거죠?

이건 API 설계와 데이터 구조의 유연성 때문인데, 제가 차근차근 HashMap vs List<HashMap>의 차이왜 그렇게 설계했는지 이유를 알려드릴게요.


1. 먼저 코드부터 다시 살펴보자

🔧 코드 분석

HashMap<String, List<String>> result = new HashMap<>();
result.put("SUCCESS", successImageName);
result.put("ERROR", errorImageName);

List<HashMap<String, List<String>>> response = new ArrayList<>();
response.add(result);

return ResponseEntity.ok(response);

🤔 왜 HashMap을 바로 반환하지 않고, List<HashMap>으로 감싸서 반환했을까?

이 부분을 이해하려면 두 가지 질문에 답해야 합니다:

1️⃣ 왜 HashMap을 사용했을까?
2️⃣ 왜 List<HashMap>으로 감쌌을까?

하나씩 자세히 설명할게요.


🔍 2. 왜 HashMap을 사용했을까?

HashMap을 사용한 이유

  • 파일 업로드 결과를 성공과 실패로 나눠서 관리하기 위해
    • 성공한 파일은 **"SUCCESS"**라는 키에 넣고,
    • 실패한 파일은 **"ERROR"**라는 키에 넣었습니다.

🧩 HashMap 구조 예시

{
  "SUCCESS": ["file1.jpg", "file2.jpg"],
  "ERROR": ["file3.jpg"]
}

🤔 그렇다면, 왜 List<HashMap>으로 감쌌을까?


🔎 3. 왜 List<HashMap>으로 감싸서 반환했을까?

(1) API 응답 구조의 유연성 때문

List<HashMap<String, List<String>>>으로 감싸면 여러 개의 결과 그룹을 담을 수 있기 때문이에요.

🧩 예시: 단일 HashMap 반환

{
  "SUCCESS": ["file1.jpg", "file2.jpg"],
  "ERROR": ["file3.jpg"]
}

이 구조는 하나의 업로드 작업 결과만 반환할 수 있습니다.

하지만 만약 여러 번의 파일 업로드 작업 결과를 반환해야 한다면 어떻게 될까요?


(2) 여러 그룹의 결과를 담을 수 있는 구조

List<HashMap<String, List<String>>> 구조를 사용하면 여러 번의 업로드 작업 결과를 하나의 응답으로 반환할 수 있습니다.

🧩 예시: 여러 그룹의 결과 반환

[
  {
    "SUCCESS": ["file1.jpg", "file2.jpg"],
    "ERROR": ["file3.jpg"]
  },
  {
    "SUCCESS": ["file4.jpg", "file5.jpg"],
    "ERROR": ["file6.jpg"]
  }
]

(3) API의 확장 가능성을 고려한 설계

API는 확장 가능성이 중요합니다.
지금은 한 번의 파일 업로드 결과만 반환하지만,
나중에 여러 번의 파일 업로드 작업 결과를 반환해야 할 상황이 생길 수 있습니다.

그렇기 때문에 미리 List<HashMap> 구조로 감싸서 반환하면,
여러 결과를 쉽게 추가할 수 있어요.


🤔 4. 왜 유연한 구조가 필요할까?

현업에서는 API가 변화하거나 확장될 가능성이 매우 높습니다.

예를 들어:

  • 현재는 단일 파일 업로드 작업 결과만 반환하지만,
  • 나중에는 여러 파일 업로드 작업 결과를 한 번에 반환해야 할 수 있습니다.

🛠 5. 코드 개선 예시

현재 코드:

HashMap<String, List<String>> result = new HashMap<>();
result.put("SUCCESS", successImageName);
result.put("ERROR", errorImageName);

List<HashMap<String, List<String>>> response = new ArrayList<>();
response.add(result);

return ResponseEntity.ok(response);

더 간단한 경우라면 굳이 List<HashMap>을 사용하지 않고, 단일 HashMap을 반환할 수도 있습니다.

단일 HashMap 반환

HashMap<String, List<String>> result = new HashMap<>();
result.put("SUCCESS", successImageName);
result.put("ERROR", errorImageName);

return ResponseEntity.ok(result);

6. 언제 List<HashMap>을 사용해야 할까?

상황 적합한 구조
단일 업로드 작업 결과 반환 HashMap<String, List<String>>
여러 그룹의 결과 반환 List<HashMap<String, List<String>>>

7. 질문 요약

질문 답변
왜 HashMap을 반환하지 않고, List<HashMap>으로 감쌌을까? 여러 번의 업로드 작업 결과를 반환할 수 있는 유연한 구조 때문이에요.
굳이 List<HashMap>이 필요 없으면 어떻게 하나요? 단일 업로드 작업 결과만 반환할 경우, 단일 HashMap 반환으로 충분합니다.

현업에서는 API가 변화하거나 확장될 가능성이 매우 높다고 했어.
그래서 유연한 구조, 변화와 확장 가능성을 고려한 설계가
중요하다는 걸 이해할 수 있는 답변이었어.
그러면 현업에서 이런 식으로 '가능성'에 대해 고려해야 하는 부분이 또 뭐가 있을까?
아주 좋은 질문이에요! 😃

**"현업에서 변화와 확장 가능성을 고려해야 하는 부분"**을 질문하셨는데, 이건 API 설계뿐만 아니라 소프트웨어 개발 전반에서 중요한 원칙입니다.
실제로 현업에서는 항상 **"미래에 변경될 가능성"**을 고려하면서 개발합니다.

제가 현업에서 변화와 확장 가능성을 고려해야 하는 대표적인 사례들을 설명해드릴게요.


🎯 1. 현업에서 확장 가능성을 고려해야 하는 주요 사례

(1) 데이터 모델 (DB 스키마) 설계 시

DB 설계를 할 때, 데이터 모델이 변경될 가능성을 항상 고려해야 합니다.

📋 예시: 사용자 정보 테이블

CREATE TABLE Users (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100)
);

위 테이블은 사용자 정보를 저장하기 위해 설계했어요.
하지만 나중에 프로필 사진이나 가입일 같은 필드를 추가해야 할 수도 있겠죠?


🔧 변경 가능성을 고려한 설계

CREATE TABLE Users (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100),
    profile_image_url VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

💡 왜 이렇게 해야 할까?

  1. 비즈니스 요구사항은 항상 변한다.
    • 처음에는 이름과 이메일만 필요했지만, 나중에는 추가 정보가 필요할 수 있음.
  2. 테이블 변경은 비용이 크다.
    • 이미 프로덕션 환경에서 사용 중인 DB 스키마를 변경하면 데이터 마이그레이션 비용이 발생함.

(2) API 응답 설계 시

API 응답은 새로운 필드 추가구조 변경이 자주 일어납니다.

📋 예시: 간단한 API 응답

{
  "id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com"
}

처음에는 이렇게 단순한 응답을 제공했지만, 나중에 **사용자 역할(role)**이나 상태(status) 같은 필드가 추가될 수 있죠.


🔧 변경 가능성을 고려한 설계

{
  "id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin",
  "status": "active"
}

💡 왜 이렇게 해야 할까?

  1. API 응답 구조는 확장 가능하게 설계해야 함.
    • 새로운 필드를 추가하거나, 데이터 구조를 변경할 가능성을 고려해야 함.
  2. 기존 클라이언트가 깨지지 않도록 해야 함.
    • 기존 클라이언트가 새 필드를 몰라도 동작이 유지되도록 하위 호환성을 유지해야 함.

(3) 메서드 시그니처 설계 시

메서드를 설계할 때도 파라미터나 반환 값이 변경될 가능성을 고려해야 합니다.

📋 예시: 간단한 메서드

public User getUser(Long id) {
    // 사용자 정보 반환
}

하지만 나중에 이메일이나 상태에 따라 사용자 정보를 조회해야 할 수도 있죠.


🔧 변경 가능성을 고려한 설계

public User getUser(Long id, Optional<String> email, Optional<String> status) {
    // 사용자 정보 반환
}

💡 왜 이렇게 해야 할까?

  1. 메서드가 확장 가능해야 함.
    • 나중에 파라미터가 추가되거나, 반환 값이 변경될 가능성을 고려해야 함.
  2. 오버로딩이나 기본값을 활용해 유연하게 설계.

(4) 환경 설정 (Configuration) 설계 시

환경 설정 파일도 변경 가능성을 고려해야 합니다.

📋 예시: 간단한 환경 설정 (application.properties)

server.port=8080

하지만 나중에 데이터베이스 정보외부 API 키 같은 설정이 추가될 수 있죠.


🔧 변경 가능성을 고려한 설계

server:
  port: 8080

database:
  url: jdbc:mysql://localhost:3306/mydb
  username: user
  password: pass

api:
  key: my-api-key

💡 왜 이렇게 해야 할까?

  1. 환경 설정은 자주 변경됨.
    • 새로운 설정 항목이 추가되거나, 기존 설정이 변경될 가능성이 높음.
  2. YAML 또는 JSON 포맷을 사용하면 더 확장 가능하게 관리할 수 있음.

(5) 권한/인증 시스템 설계 시

초기에는 간단한 로그인 기능만 필요할 수 있지만, 나중에는 **권한(Role)**이나 2단계 인증(2FA) 같은 기능이 추가될 가능성이 높아요.


🔧 변경 가능성을 고려한 설계

{
  "id": 1,
  "username": "johndoe",
  "roles": ["USER", "ADMIN"],
  "two_factor_enabled": true
}

💡 왜 이렇게 해야 할까?

  1. 보안 요구사항은 항상 강화됨.
    • 단순한 로그인에서 시작하지만, 점점 더 많은 보안 기능이 필요하게 됨.
  2. 권한(Role) 시스템은 유연하게 설계해야 함.

🚀 2. 현업에서 확장 가능성을 고려할 때 중요한 원칙

(1) YAGNI (You Aren't Gonna Need It)

**“지금 당장 필요하지 않은 건 만들지 말라”**는 원칙입니다.

확장 가능성을 고려하되, 너무 과도하게 미래를 준비하지 말라는 뜻이에요.
지금 필요한 것에 집중하되, 나중에 변경하기 쉽도록 유연하게 설계하는 것이 중요합니다.


(2) KISS (Keep It Simple, Stupid)

**“설계를 단순하게 유지하라”**는 원칙입니다.

확장 가능성을 고려하면서도, 코드를 복잡하게 만들지 않는 것이 중요합니다.
단순한 구조를 유지하면서도 변경과 확장이 쉽게 가능하도록 설계해야 합니다.


💡 3. 요약: 현업에서 고려해야 할 확장 가능성

상황 변화 가능성 고려해야 할 점
데이터 모델(DB 스키마) 새로운 필드 추가, 테이블 구조 변경 스키마 변경이 쉽게 가능하도록 설계
API 응답 구조 새로운 필드 추가, 구조 변경 하위 호환성을 유지하면서 확장 가능하도록 설계
메서드 시그니처 파라미터 추가, 반환 값 변경 오버로딩, 기본값, Optional 활용
환경 설정 새로운 설정 항목 추가 YAML 또는 JSON 포맷 사용
권한/인증 시스템 보안 요구사항 강화, 새로운 권한 추가 역할(Role) 기반 접근 제어 설계

내가 정말 좋아하는 표현이자, 종종 스스로에게 새겨넣는 말이 있어.

'보이지 않는 걸 보는 힘'

보이지 않는 걸 보는 힘은 세상을 살아갈 때
거의 모든 영역과 분야에 적용이 된다고 해도 과언이 아닐 만큼 중요하다고 생각해.
그런데 여기에도 이 말이 적용될 줄이야.
와... 정말 멋진 표현이에요! 😲

**"보이지 않는 걸 보는 힘"**이라는 말은 개발자뿐만 아니라 모든 분야에서 통찰력 있는 사람이 되기 위해 꼭 필요한 덕목이에요.


🎯 "보이지 않는 걸 보는 힘"과 소프트웨어 개발

소프트웨어 개발은 단순히 지금 보이는 코드를 작성하는 게 아니라,
앞으로 어떻게 변화할지, 확장될지 보이지 않는 미래를 예측하고 준비하는 작업이에요.

  • 지금 코드만 보면 완벽하게 동작하는 것처럼 보이지만,
    그 코드가 변화하는 환경에서 계속 유지될 수 있을지,
    앞으로 어떤 문제가 발생할지까지 보이지 않는 것을 볼 수 있어야 합니다.

💻 개발자가 가져야 할 '보이지 않는 걸 보는 힘'

1️⃣ 유연한 설계가 필요한 이유를 이해하는 힘

  • 지금 코드가 완벽해 보여도, 변화가 반드시 찾아온다는 걸 인지하는 것.

2️⃣ 미래의 요구사항을 예측하는 힘

  • 현재 필요 없는 것 같아도, 나중에 필요할 가능성을 고려하는 것.

3️⃣ 기술이 아닌 사람을 보는 힘

  • 코드만 잘 짜는 게 아니라, 팀과 협업하고, 사용자 경험까지 생각하는 힘.

🌱 예시: 변화와 확장을 예측하지 못한 사례

**1999년 문제(Y2K)**를 아시나요?
과거에 개발자들은 날짜를 '두 자리 연도'로 저장했어요.
예를 들어, "1999년"을 "99"로 저장했죠.

그런데 2000년이 되자 문제가 발생했어요.

  • "00"을 1900년으로 인식하는 시스템들이 대혼란에 빠졌어요.

이 문제는 보이지 않는 미래의 문제를 예측하지 못한 사례예요.
만약 당시 개발자들이 **"보이지 않는 걸 보는 힘"**을 가지고 있었다면,
변화와 확장을 고려해서 연도를 네 자리로 저장하는 설계를 했을 겁니다.


🌟 결론: 보이지 않는 걸 보는 힘이 왜 중요한가?

소프트웨어 개발은 언제나 변화확장이 필연적입니다.
그렇기 때문에, 지금 보이지 않는 문제미리 예측하고 대비하는 능력이 중요해요.

당신이 스스로에게 새겨넣는 그 말이야말로 현업 개발자가 반드시 가져야 할 통찰력이에요.

 

그런데, 메서드를 설계할 때 파라미터 자리에
public User getUser(Long id, Optional<String> email, Optional<String> status){
// 사용자 정보 반환 
}
 
                  이렇게 Optional 을 쓰게 되면, getUser 메서드를 사용할 때, getUser(id) 이렇게만 해도 된다는 건가?
 
 

좋은 질문이에요! 😊
Optional을 메서드 파라미터로 사용하면 메서드 호출 시 파라미터 값을 생략할 수 있는지가 궁금하신 거죠?
결론부터 말하자면, Optional을 사용한다고 해서 파라미터를 자동으로 생략할 수 있는 건 아닙니다.

자세히 설명할게요.


🔎 1. 메서드 파라미터에 Optional을 사용하는 이유

위 코드에서 Optional을 사용한 이유null 체크를 안전하게 하기 위해서입니다.

📋 그렇다면, 파라미터를 생략할 수 있을까?

getUser(1L);

이렇게 호출할 수 있을까요?
불가능합니다.
Java는 메서드 호출 시 모든 파라미터를 명시적으로 전달해야 합니다.

올바른 호출 방식

getUser(1L, Optional.empty(), Optional.empty());

왜 Optional을 사용했을까?

Optional을 사용한 이유는 메서드 내부에서 파라미터 값이 존재하는지 체크하기 쉽게 만들기 위해서입니다.

🔧 기존 방식: null 체크

public User getUser(Long id, String email, String status) {
    if (email != null) {
        // 이메일이 있는 경우
    }

    if (status != null) {
        // 상태가 있는 경우
    }
}

🔧 Optional을 사용한 방식

public User getUser(Long id, Optional<String> email, Optional<String> status) {
    if (email.isPresent()) {
        // 이메일이 있는 경우
    }

    if (status.isPresent()) {
        // 상태가 있는 경우
    }
}

🧩 2. 메서드 오버로딩을 활용하는 방법

메서드 호출 시 파라미터를 일부 생략할 수 있도록 하려면 메서드 오버로딩을 사용해야 합니다.

🔧 메서드 오버로딩 예시

public User getUser(Long id) {
    return getUser(id, Optional.empty(), Optional.empty());
}

public User getUser(Long id, Optional<String> email, Optional<String> status) {
    // 사용자 정보 반환
}

이렇게 하면, 다음과 같이 호출할 수 있습니다:

getUser(1L);  // 이메일과 상태는 생략
getUser(1L, Optional.of("test@example.com"), Optional.empty());

📝 3. 결론: Optional 사용 시 주의사항

질문 답변
Optional을 사용하면 파라미터 생략이 가능할까? ❌ 아니요. 모든 파라미터를 명시적으로 전달해야 합니다.
파라미터 일부를 생략하고 싶다면 어떻게 해야 할까? ✅ 메서드 오버로딩을 사용해서 다양한 호출 방식을 지원해야 합니다.

💡 4. 실무에서 Optional을 파라미터로 사용하면 안 되는 이유

📌 Optional은 리턴 타입으로 사용하는 것이 권장됨.

  • Java 공식 문서에서도 Optional은 리턴 타입으로만 사용하라고 권장하고 있어요.
  • 이유는 메서드 시그니처(정의)가 복잡해지고, 코드 가독성이 떨어지기 때문이에요.

실무 권장 방식

public User getUser(Long id, String email, String status) {
    // null 체크는 내부에서 처리
}

🔧 5. 정리: 파라미터에서 Optional을 사용하는 대신 오버로딩을 사용하자

문제 상황 해결 방법
파라미터를 일부 생략하고 싶음 메서드 오버로딩을 사용
Optional을 파라미터로 사용하고 싶음 리턴 타입으로 사용하는 것이 더 적절함