질문 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
);
💡 왜 이렇게 해야 할까?
- 비즈니스 요구사항은 항상 변한다.
- 처음에는 이름과 이메일만 필요했지만, 나중에는 추가 정보가 필요할 수 있음.
- 테이블 변경은 비용이 크다.
- 이미 프로덕션 환경에서 사용 중인 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"
}
💡 왜 이렇게 해야 할까?
- API 응답 구조는 확장 가능하게 설계해야 함.
- 새로운 필드를 추가하거나, 데이터 구조를 변경할 가능성을 고려해야 함.
- 기존 클라이언트가 깨지지 않도록 해야 함.
- 기존 클라이언트가 새 필드를 몰라도 동작이 유지되도록 하위 호환성을 유지해야 함.
✅ (3) 메서드 시그니처 설계 시
메서드를 설계할 때도 파라미터나 반환 값이 변경될 가능성을 고려해야 합니다.
📋 예시: 간단한 메서드
public User getUser(Long id) {
// 사용자 정보 반환
}
하지만 나중에 이메일이나 상태에 따라 사용자 정보를 조회해야 할 수도 있죠.
🔧 변경 가능성을 고려한 설계
public User getUser(Long id, Optional<String> email, Optional<String> status) {
// 사용자 정보 반환
}
💡 왜 이렇게 해야 할까?
- 메서드가 확장 가능해야 함.
- 나중에 파라미터가 추가되거나, 반환 값이 변경될 가능성을 고려해야 함.
- 오버로딩이나 기본값을 활용해 유연하게 설계.
✅ (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
💡 왜 이렇게 해야 할까?
- 환경 설정은 자주 변경됨.
- 새로운 설정 항목이 추가되거나, 기존 설정이 변경될 가능성이 높음.
- YAML 또는 JSON 포맷을 사용하면 더 확장 가능하게 관리할 수 있음.
✅ (5) 권한/인증 시스템 설계 시
초기에는 간단한 로그인 기능만 필요할 수 있지만, 나중에는 **권한(Role)**이나 2단계 인증(2FA) 같은 기능이 추가될 가능성이 높아요.
🔧 변경 가능성을 고려한 설계
{
"id": 1,
"username": "johndoe",
"roles": ["USER", "ADMIN"],
"two_factor_enabled": true
}
💡 왜 이렇게 해야 할까?
- 보안 요구사항은 항상 강화됨.
- 단순한 로그인에서 시작하지만, 점점 더 많은 보안 기능이 필요하게 됨.
- 권한(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을 메서드 파라미터로 사용하면 메서드 호출 시 파라미터 값을 생략할 수 있는지가 궁금하신 거죠?
결론부터 말하자면, 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을 파라미터로 사용하고 싶음 | 리턴 타입으로 사용하는 것이 더 적절함 |
'Spring Boot (+ RESTful)' 카테고리의 다른 글
RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (10) (0) | 2025.01.12 |
---|---|
RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (9) (0) | 2025.01.11 |
RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (7) (0) | 2025.01.09 |
RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (6) (0) | 2025.01.08 |
RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (5) (0) | 2025.01.07 |