From 76ce920304529cfa4c1c7893920a2b46fc671564 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Nov 2025 01:01:27 +0000 Subject: [PATCH] feat: Add practical real-world examples for Singleton, Builder, and Strategy patterns Added comprehensive real-world examples with tests and documentation for three commonly used design patterns in enterprise applications. ## Singleton Pattern - ConfigurationManager: Application settings management - LoggerService: Centralized logging system - DatabaseConnectionPool: Connection pool management - Full test coverage for all implementations - Updated README with usage examples and Korean documentation ## Builder Pattern - UserDTO: Complex user data transfer object - HttpRequest: HTTP request builder for API calls - EmailMessage: Email composition with attachments - Comprehensive tests for all builders - Enhanced README with practical use cases ## Strategy Pattern - PaymentProcessor: Payment processing system - CreditCardPayment: Credit card payment strategy - BankTransferPayment: Bank transfer strategy - PayPalPayment: PayPal integration strategy - Complete test suite for payment strategies - Updated README with e-commerce payment example All examples include: - Production-ready code with validation - Comprehensive JUnit 5 tests - Detailed Javadoc documentation - Korean and English README sections - Real-world use cases and best practices --- builder/README.md | 105 +++++ .../com/iluwatar/builder/EmailMessage.java | 396 ++++++++++++++++ .../com/iluwatar/builder/HttpRequest.java | 390 +++++++++++++++ .../java/com/iluwatar/builder/UserDTO.java | 348 ++++++++++++++ .../iluwatar/builder/EmailMessageTest.java | 445 ++++++++++++++++++ .../com/iluwatar/builder/HttpRequestTest.java | 351 ++++++++++++++ .../com/iluwatar/builder/UserDTOTest.java | 279 +++++++++++ singleton/README.md | 97 ++++ .../singleton/ConfigurationManager.java | 172 +++++++ .../singleton/DatabaseConnectionPool.java | 309 ++++++++++++ .../com/iluwatar/singleton/LoggerService.java | 231 +++++++++ .../singleton/ConfigurationManagerTest.java | 230 +++++++++ .../singleton/DatabaseConnectionPoolTest.java | 369 +++++++++++++++ .../iluwatar/singleton/LoggerServiceTest.java | 301 ++++++++++++ strategy/README.md | 71 ++- .../strategy/payment/BankTransferPayment.java | 134 ++++++ .../strategy/payment/CreditCardPayment.java | 120 +++++ .../strategy/payment/PayPalPayment.java | 124 +++++ .../strategy/payment/PaymentProcessor.java | 150 ++++++ .../strategy/payment/PaymentResult.java | 98 ++++ .../strategy/payment/PaymentStrategy.java | 56 +++ .../payment/PaymentProcessorTest.java | 208 ++++++++ 22 files changed, 4983 insertions(+), 1 deletion(-) create mode 100644 builder/src/main/java/com/iluwatar/builder/EmailMessage.java create mode 100644 builder/src/main/java/com/iluwatar/builder/HttpRequest.java create mode 100644 builder/src/main/java/com/iluwatar/builder/UserDTO.java create mode 100644 builder/src/test/java/com/iluwatar/builder/EmailMessageTest.java create mode 100644 builder/src/test/java/com/iluwatar/builder/HttpRequestTest.java create mode 100644 builder/src/test/java/com/iluwatar/builder/UserDTOTest.java create mode 100644 singleton/src/main/java/com/iluwatar/singleton/ConfigurationManager.java create mode 100644 singleton/src/main/java/com/iluwatar/singleton/DatabaseConnectionPool.java create mode 100644 singleton/src/main/java/com/iluwatar/singleton/LoggerService.java create mode 100644 singleton/src/test/java/com/iluwatar/singleton/ConfigurationManagerTest.java create mode 100644 singleton/src/test/java/com/iluwatar/singleton/DatabaseConnectionPoolTest.java create mode 100644 singleton/src/test/java/com/iluwatar/singleton/LoggerServiceTest.java create mode 100644 strategy/src/main/java/com/iluwatar/strategy/payment/BankTransferPayment.java create mode 100644 strategy/src/main/java/com/iluwatar/strategy/payment/CreditCardPayment.java create mode 100644 strategy/src/main/java/com/iluwatar/strategy/payment/PayPalPayment.java create mode 100644 strategy/src/main/java/com/iluwatar/strategy/payment/PaymentProcessor.java create mode 100644 strategy/src/main/java/com/iluwatar/strategy/payment/PaymentResult.java create mode 100644 strategy/src/main/java/com/iluwatar/strategy/payment/PaymentStrategy.java create mode 100644 strategy/src/test/java/com/iluwatar/strategy/payment/PaymentProcessorTest.java diff --git a/builder/README.md b/builder/README.md index ccfc378d6e36..33294aebd094 100644 --- a/builder/README.md +++ b/builder/README.md @@ -129,6 +129,111 @@ Use the Builder pattern when * [Apache Camel builders](https://github.com/apache/camel/tree/0e195428ee04531be27a0b659005e3aa8d159d23/camel-core/src/main/java/org/apache/camel/builder) * [Apache Commons Option.Builder](https://commons.apache.org/proper/commons-cli/apidocs/org/apache/commons/cli/Option.Builder.html) +## 실무 예제 (Practical Examples) + +### 1. UserDTO - 사용자 정보 DTO + +API 요청/응답에서 사용되는 복잡한 사용자 정보 객체를 Builder 패턴으로 구성합니다. + +```java +UserDTO user = UserDTO.builder() + .id(1L) + .username("john.doe") + .email("john@example.com") + .firstName("John") + .lastName("Doe") + .age(30) + .phoneNumber("010-1234-5678") + .address("123 Main St, Seoul") + .addRole("USER") + .addRole("ADMIN") + .active(true) + .build(); + +System.out.println(user.getFullName()); // "John Doe" +System.out.println(user.hasRole("ADMIN")); // true +``` + +**실무 활용:** +- API 요청/응답 DTO 생성 +- 데이터베이스 엔티티에서 DTO로 변환 +- 테스트 데이터 생성 +- 복잡한 사용자 프로필 구성 + +### 2. HttpRequest - HTTP 요청 빌더 + +REST API 호출을 위한 HTTP 요청을 체계적으로 구성합니다. + +```java +HttpRequest request = HttpRequest.builder() + .post() + .url("https://api.example.com/users") + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", "Bearer token123") + .addQueryParam("page", "1") + .addQueryParam("size", "10") + .jsonBody("{\"name\": \"John Doe\"}") + .timeout(5000) + .build(); + +String response = request.execute(); +``` + +**실무 활용:** +- REST API 클라이언트 구현 +- HTTP 라이브러리 래퍼 +- 마이크로서비스 간 통신 +- 테스트용 HTTP 요청 생성 + +### 3. EmailMessage - 이메일 메시지 빌더 + +복잡한 이메일 메시지를 단계적으로 구성합니다. + +```java +EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .addCc("manager@example.com") + .addBcc("admin@example.com") + .subject("Welcome to our service!") + .body("

Welcome!

Thank you for joining us.

") + .html(true) + .priority(Priority.HIGH) + .addAttachment("/path/to/document.pdf") + .build(); + +boolean sent = email.send(); +System.out.println("Total recipients: " + email.getTotalRecipientCount()); +``` + +**실무 활용:** +- 이메일 발송 시스템 +- 알림 서비스 +- 마케팅 이메일 생성 +- 시스템 알림 메일 + +## Builder 패턴의 실무 장점 + +1. **가독성**: 복잡한 객체 생성 과정을 명확하게 표현 +2. **유연성**: 선택적 파라미터를 쉽게 처리 +3. **불변성**: Immutable 객체 생성으로 Thread-safe 보장 +4. **유효성 검증**: build() 메서드에서 일괄 유효성 검증 +5. **유지보수성**: 새로운 속성 추가가 용이 + +## 테스트 실행 + +각 실무 예제에는 comprehensive한 테스트 코드가 포함되어 있습니다: + +```bash +# 모든 테스트 실행 +mvn test + +# 특정 테스트만 실행 +mvn test -Dtest=UserDTOTest +mvn test -Dtest=HttpRequestTest +mvn test -Dtest=EmailMessageTest +``` + ## Credits * [Design Patterns: Elements of Reusable Object-Oriented Software](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) diff --git a/builder/src/main/java/com/iluwatar/builder/EmailMessage.java b/builder/src/main/java/com/iluwatar/builder/EmailMessage.java new file mode 100644 index 000000000000..40d5aa8d2fe7 --- /dev/null +++ b/builder/src/main/java/com/iluwatar/builder/EmailMessage.java @@ -0,0 +1,396 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.builder; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 실무 예제: 이메일 메시지 빌더 + * + *

EmailMessage는 이메일을 구성하는 Builder 패턴 예제입니다. + * 복잡한 이메일 메시지를 단계적으로 구성할 수 있습니다. + * + *

실무 활용 사례:

+ * + * + *

사용 예제:

+ *
+ * EmailMessage email = EmailMessage.builder()
+ *     .from("sender@example.com")
+ *     .to("recipient@example.com")
+ *     .subject("Welcome to our service!")
+ *     .body("Thank you for joining us.")
+ *     .addCc("manager@example.com")
+ *     .addBcc("admin@example.com")
+ *     .priority(Priority.HIGH)
+ *     .html(true)
+ *     .build();
+ *
+ * // 이메일 전송 시뮬레이션
+ * boolean sent = email.send();
+ * 
+ * + *

장점:

+ * + */ +public final class EmailMessage { + + private final String from; + private final List to; + private final List cc; + private final List bcc; + private final String subject; + private final String body; + private final boolean isHtml; + private final Priority priority; + private final List attachments; + private final LocalDateTime createdAt; + + /** + * 이메일 우선순위 열거형. + */ + public enum Priority { + LOW, NORMAL, HIGH, URGENT + } + + /** + * Private 생성자 - Builder를 통해서만 객체 생성 가능. + */ + private EmailMessage(Builder builder) { + this.from = builder.from; + this.to = Collections.unmodifiableList(new ArrayList<>(builder.to)); + this.cc = Collections.unmodifiableList(new ArrayList<>(builder.cc)); + this.bcc = Collections.unmodifiableList(new ArrayList<>(builder.bcc)); + this.subject = builder.subject; + this.body = builder.body; + this.isHtml = builder.isHtml; + this.priority = builder.priority; + this.attachments = Collections.unmodifiableList(new ArrayList<>(builder.attachments)); + this.createdAt = LocalDateTime.now(); + } + + /** + * Builder 인스턴스 생성. + * + * @return EmailMessage Builder + */ + public static Builder builder() { + return new Builder(); + } + + // Getters + public String getFrom() { + return from; + } + + public List getTo() { + return to; + } + + public List getCc() { + return cc; + } + + public List getBcc() { + return bcc; + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + public boolean isHtml() { + return isHtml; + } + + public Priority getPriority() { + return priority; + } + + public List getAttachments() { + return attachments; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + /** + * 총 수신자 수 계산. + * + * @return 총 수신자 수 (TO + CC + BCC) + */ + public int getTotalRecipientCount() { + return to.size() + cc.size() + bcc.size(); + } + + /** + * 첨부파일 존재 여부. + * + * @return 첨부파일이 있으면 true + */ + public boolean hasAttachments() { + return !attachments.isEmpty(); + } + + /** + * 이메일 전송 시뮬레이션. + * + * @return 전송 성공 여부 + */ + public boolean send() { + System.out.println("=".repeat(50)); + System.out.println("Sending Email:"); + System.out.println("From: " + from); + System.out.println("To: " + String.join(", ", to)); + + if (!cc.isEmpty()) { + System.out.println("CC: " + String.join(", ", cc)); + } + if (!bcc.isEmpty()) { + System.out.println("BCC: " + String.join(", ", bcc)); + } + + System.out.println("Subject: " + subject); + System.out.println("Priority: " + priority); + System.out.println("Format: " + (isHtml ? "HTML" : "Plain Text")); + + if (!attachments.isEmpty()) { + System.out.println("Attachments: " + String.join(", ", attachments)); + } + + System.out.println("\nBody:"); + System.out.println(body); + System.out.println("=".repeat(50)); + System.out.println("Email sent successfully!"); + + return true; + } + + @Override + public String toString() { + return "EmailMessage{" + + "from='" + from + '\'' + + ", to=" + to.size() + " recipients" + + ", subject='" + subject + '\'' + + ", priority=" + priority + + ", attachments=" + attachments.size() + + ", createdAt=" + createdAt + + '}'; + } + + /** + * EmailMessage Builder 클래스. + */ + public static class Builder { + private String from; + private List to = new ArrayList<>(); + private List cc = new ArrayList<>(); + private List bcc = new ArrayList<>(); + private String subject; + private String body; + private boolean isHtml = false; + private Priority priority = Priority.NORMAL; + private List attachments = new ArrayList<>(); + + /** + * 발신자 설정 (필수). + * + * @param from 발신자 이메일 + * @return Builder + */ + public Builder from(String from) { + this.from = from; + return this; + } + + /** + * 수신자 추가 (필수). + * + * @param to 수신자 이메일 + * @return Builder + */ + public Builder to(String to) { + this.to.add(to); + return this; + } + + /** + * 여러 수신자 추가. + * + * @param recipients 수신자 목록 + * @return Builder + */ + public Builder toMultiple(List recipients) { + this.to.addAll(recipients); + return this; + } + + /** + * 참조(CC) 추가. + * + * @param cc 참조 이메일 + * @return Builder + */ + public Builder addCc(String cc) { + this.cc.add(cc); + return this; + } + + /** + * 숨은참조(BCC) 추가. + * + * @param bcc 숨은참조 이메일 + * @return Builder + */ + public Builder addBcc(String bcc) { + this.bcc.add(bcc); + return this; + } + + /** + * 제목 설정 (필수). + * + * @param subject 이메일 제목 + * @return Builder + */ + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + /** + * 본문 설정 (필수). + * + * @param body 이메일 본문 + * @return Builder + */ + public Builder body(String body) { + this.body = body; + return this; + } + + /** + * HTML 형식 설정. + * + * @param isHtml HTML 형식 여부 + * @return Builder + */ + public Builder html(boolean isHtml) { + this.isHtml = isHtml; + return this; + } + + /** + * 우선순위 설정. + * + * @param priority 이메일 우선순위 + * @return Builder + */ + public Builder priority(Priority priority) { + this.priority = priority; + return this; + } + + /** + * 첨부파일 추가. + * + * @param filePath 첨부파일 경로 + * @return Builder + */ + public Builder addAttachment(String filePath) { + this.attachments.add(filePath); + return this; + } + + /** + * 여러 첨부파일 추가. + * + * @param filePaths 첨부파일 경로 목록 + * @return Builder + */ + public Builder attachments(List filePaths) { + this.attachments.addAll(filePaths); + return this; + } + + /** + * EmailMessage 객체 생성. + * 필수 필드 유효성 검증 수행. + * + * @return EmailMessage 객체 + * @throws IllegalStateException 필수 필드가 없거나 유효하지 않은 경우 + */ + public EmailMessage build() { + if (from == null || from.trim().isEmpty()) { + throw new IllegalStateException("From address is required"); + } + if (!isValidEmail(from)) { + throw new IllegalStateException("Invalid from email address"); + } + if (to.isEmpty()) { + throw new IllegalStateException("At least one recipient is required"); + } + for (String recipient : to) { + if (!isValidEmail(recipient)) { + throw new IllegalStateException("Invalid recipient email address: " + recipient); + } + } + if (subject == null || subject.trim().isEmpty()) { + throw new IllegalStateException("Subject is required"); + } + if (body == null || body.trim().isEmpty()) { + throw new IllegalStateException("Body is required"); + } + return new EmailMessage(this); + } + + /** + * 이메일 주소 유효성 검증 (간단한 검증). + * + * @param email 이메일 주소 + * @return 유효하면 true + */ + private boolean isValidEmail(String email) { + return email != null && email.contains("@") && email.contains("."); + } + } +} diff --git a/builder/src/main/java/com/iluwatar/builder/HttpRequest.java b/builder/src/main/java/com/iluwatar/builder/HttpRequest.java new file mode 100644 index 000000000000..b34de9e7b2c9 --- /dev/null +++ b/builder/src/main/java/com/iluwatar/builder/HttpRequest.java @@ -0,0 +1,390 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.builder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * 실무 예제: HTTP 요청 빌더 + * + *

HttpRequest는 HTTP 요청을 구성하는 Builder 패턴 예제입니다. + * 복잡한 HTTP 요청을 단계적으로 구성할 수 있습니다. + * + *

실무 활용 사례:

+ *
    + *
  • REST API 클라이언트 구현
  • + *
  • HTTP 라이브러리 래퍼
  • + *
  • 테스트용 HTTP 요청 생성
  • + *
  • 마이크로서비스 간 통신
  • + *
+ * + *

사용 예제:

+ *
+ * HttpRequest request = HttpRequest.builder()
+ *     .method(HttpMethod.POST)
+ *     .url("https://api.example.com/users")
+ *     .addHeader("Content-Type", "application/json")
+ *     .addHeader("Authorization", "Bearer token123")
+ *     .body("{\"name\": \"John Doe\"}")
+ *     .timeout(5000)
+ *     .build();
+ *
+ * // 요청 실행 시뮬레이션
+ * String response = request.execute();
+ * 
+ * + *

장점:

+ *
    + *
  • 복잡한 HTTP 요청을 명확하게 표현
  • + *
  • 선택적 헤더와 파라미터 쉽게 추가
  • + *
  • Fluent API로 가독성 향상
  • + *
  • 불변 객체로 Thread-safe
  • + *
+ */ +public final class HttpRequest { + + private final HttpMethod method; + private final String url; + private final Map headers; + private final Map queryParams; + private final String body; + private final Integer timeout; + private final boolean followRedirects; + + /** + * HTTP 메서드 열거형. + */ + public enum HttpMethod { + GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS + } + + /** + * Private 생성자 - Builder를 통해서만 객체 생성 가능. + */ + private HttpRequest(Builder builder) { + this.method = builder.method; + this.url = builder.url; + this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers)); + this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams)); + this.body = builder.body; + this.timeout = builder.timeout; + this.followRedirects = builder.followRedirects; + } + + /** + * Builder 인스턴스 생성. + * + * @return HttpRequest Builder + */ + public static Builder builder() { + return new Builder(); + } + + // Getters + public HttpMethod getMethod() { + return method; + } + + public String getUrl() { + return url; + } + + public Map getHeaders() { + return headers; + } + + public Map getQueryParams() { + return queryParams; + } + + public String getBody() { + return body; + } + + public Integer getTimeout() { + return timeout; + } + + public boolean isFollowRedirects() { + return followRedirects; + } + + /** + * 완전한 URL 생성 (쿼리 파라미터 포함). + * + * @return 완전한 URL + */ + public String getFullUrl() { + if (queryParams.isEmpty()) { + return url; + } + + StringBuilder fullUrl = new StringBuilder(url); + fullUrl.append("?"); + + boolean first = true; + for (Map.Entry entry : queryParams.entrySet()) { + if (!first) { + fullUrl.append("&"); + } + fullUrl.append(entry.getKey()).append("=").append(entry.getValue()); + first = false; + } + + return fullUrl.toString(); + } + + /** + * HTTP 요청 실행 시뮬레이션. + * + * @return 응답 문자열 + */ + public String execute() { + StringBuilder result = new StringBuilder(); + result.append("Executing HTTP Request:\n"); + result.append("Method: ").append(method).append("\n"); + result.append("URL: ").append(getFullUrl()).append("\n"); + + if (!headers.isEmpty()) { + result.append("Headers:\n"); + headers.forEach((key, value) -> + result.append(" ").append(key).append(": ").append(value).append("\n") + ); + } + + if (body != null) { + result.append("Body: ").append(body).append("\n"); + } + + result.append("Timeout: ").append(timeout != null ? timeout + "ms" : "default").append("\n"); + result.append("Follow Redirects: ").append(followRedirects).append("\n"); + result.append("\nResponse: 200 OK (simulated)"); + + return result.toString(); + } + + @Override + public String toString() { + return "HttpRequest{" + + "method=" + method + + ", url='" + url + '\'' + + ", headers=" + headers.size() + + ", queryParams=" + queryParams.size() + + ", hasBody=" + (body != null) + + ", timeout=" + timeout + + '}'; + } + + /** + * HttpRequest Builder 클래스. + */ + public static class Builder { + private HttpMethod method = HttpMethod.GET; + private String url; + private Map headers = new HashMap<>(); + private Map queryParams = new HashMap<>(); + private String body; + private Integer timeout = 30000; // 기본 30초 + private boolean followRedirects = true; + + /** + * HTTP 메서드 설정. + * + * @param method HTTP 메서드 + * @return Builder + */ + public Builder method(HttpMethod method) { + this.method = method; + return this; + } + + /** + * GET 메서드 설정 (편의 메서드). + * + * @return Builder + */ + public Builder get() { + return method(HttpMethod.GET); + } + + /** + * POST 메서드 설정 (편의 메서드). + * + * @return Builder + */ + public Builder post() { + return method(HttpMethod.POST); + } + + /** + * PUT 메서드 설정 (편의 메서드). + * + * @return Builder + */ + public Builder put() { + return method(HttpMethod.PUT); + } + + /** + * DELETE 메서드 설정 (편의 메서드). + * + * @return Builder + */ + public Builder delete() { + return method(HttpMethod.DELETE); + } + + /** + * URL 설정 (필수). + * + * @param url 요청 URL + * @return Builder + */ + public Builder url(String url) { + this.url = url; + return this; + } + + /** + * 헤더 추가. + * + * @param name 헤더 이름 + * @param value 헤더 값 + * @return Builder + */ + public Builder addHeader(String name, String value) { + this.headers.put(name, value); + return this; + } + + /** + * 여러 헤더 추가. + * + * @param headers 헤더 맵 + * @return Builder + */ + public Builder headers(Map headers) { + this.headers.putAll(headers); + return this; + } + + /** + * 쿼리 파라미터 추가. + * + * @param name 파라미터 이름 + * @param value 파라미터 값 + * @return Builder + */ + public Builder addQueryParam(String name, String value) { + this.queryParams.put(name, value); + return this; + } + + /** + * 여러 쿼리 파라미터 추가. + * + * @param params 파라미터 맵 + * @return Builder + */ + public Builder queryParams(Map params) { + this.queryParams.putAll(params); + return this; + } + + /** + * 요청 본문 설정. + * + * @param body 요청 본문 + * @return Builder + */ + public Builder body(String body) { + this.body = body; + return this; + } + + /** + * JSON 본문 설정 (편의 메서드). + * + * @param json JSON 문자열 + * @return Builder + */ + public Builder jsonBody(String json) { + this.body = json; + this.headers.put("Content-Type", "application/json"); + return this; + } + + /** + * 타임아웃 설정 (밀리초). + * + * @param timeout 타임아웃 (밀리초) + * @return Builder + */ + public Builder timeout(Integer timeout) { + this.timeout = timeout; + return this; + } + + /** + * 리다이렉트 따라가기 설정. + * + * @param followRedirects 리다이렉트 따라가기 여부 + * @return Builder + */ + public Builder followRedirects(boolean followRedirects) { + this.followRedirects = followRedirects; + return this; + } + + /** + * HttpRequest 객체 생성. + * 필수 필드 유효성 검증 수행. + * + * @return HttpRequest 객체 + * @throws IllegalStateException 필수 필드가 없거나 유효하지 않은 경우 + */ + public HttpRequest build() { + if (url == null || url.trim().isEmpty()) { + throw new IllegalStateException("URL is required"); + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + throw new IllegalStateException("URL must start with http:// or https://"); + } + if (method == null) { + throw new IllegalStateException("HTTP method is required"); + } + if (timeout != null && timeout < 0) { + throw new IllegalStateException("Timeout cannot be negative"); + } + if ((method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH) + && body == null) { + // Warning: POST/PUT/PATCH without body is allowed but might be unusual + System.out.println("Warning: " + method + " request without body"); + } + return new HttpRequest(this); + } + } +} diff --git a/builder/src/main/java/com/iluwatar/builder/UserDTO.java b/builder/src/main/java/com/iluwatar/builder/UserDTO.java new file mode 100644 index 000000000000..40694d07d44e --- /dev/null +++ b/builder/src/main/java/com/iluwatar/builder/UserDTO.java @@ -0,0 +1,348 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.builder; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 실무 예제: 사용자 정보 DTO (Data Transfer Object) + * + *

UserDTO는 사용자 정보를 전송하기 위한 객체로, Builder 패턴을 사용하여 + * 복잡한 사용자 정보를 단계적으로 구성할 수 있습니다. + * + *

실무 활용 사례:

+ *
    + *
  • API 요청/응답 DTO 생성
  • + *
  • 데이터베이스 엔티티에서 DTO로 변환
  • + *
  • 복잡한 사용자 프로필 구성
  • + *
  • 테스트 데이터 생성
  • + *
+ * + *

사용 예제:

+ *
+ * UserDTO user = UserDTO.builder()
+ *     .id(1L)
+ *     .username("john.doe")
+ *     .email("john@example.com")
+ *     .firstName("John")
+ *     .lastName("Doe")
+ *     .age(30)
+ *     .phoneNumber("010-1234-5678")
+ *     .addRole("USER")
+ *     .addRole("ADMIN")
+ *     .build();
+ * 
+ * + *

장점:

+ *
    + *
  • 가독성 높은 객체 생성 코드
  • + *
  • 선택적 파라미터 쉽게 처리
  • + *
  • 불변(Immutable) 객체 생성 가능
  • + *
  • 객체 생성 과정과 표현 분리
  • + *
+ */ +public final class UserDTO { + + private final Long id; + private final String username; + private final String email; + private final String firstName; + private final String lastName; + private final Integer age; + private final String phoneNumber; + private final String address; + private final LocalDate birthDate; + private final boolean active; + private final List roles; + + /** + * Private 생성자 - Builder를 통해서만 객체 생성 가능. + */ + private UserDTO(Builder builder) { + this.id = builder.id; + this.username = builder.username; + this.email = builder.email; + this.firstName = builder.firstName; + this.lastName = builder.lastName; + this.age = builder.age; + this.phoneNumber = builder.phoneNumber; + this.address = builder.address; + this.birthDate = builder.birthDate; + this.active = builder.active; + this.roles = Collections.unmodifiableList(new ArrayList<>(builder.roles)); + } + + /** + * Builder 인스턴스 생성. + * + * @return UserDTO Builder + */ + public static Builder builder() { + return new Builder(); + } + + // Getters + public Long getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getEmail() { + return email; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getFullName() { + if (firstName != null && lastName != null) { + return firstName + " " + lastName; + } + return username; + } + + public Integer getAge() { + return age; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getAddress() { + return address; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public boolean isActive() { + return active; + } + + public List getRoles() { + return roles; + } + + public boolean hasRole(String role) { + return roles.contains(role); + } + + @Override + public String toString() { + return "UserDTO{" + + "id=" + id + + ", username='" + username + '\'' + + ", email='" + email + '\'' + + ", fullName='" + getFullName() + '\'' + + ", age=" + age + + ", active=" + active + + ", roles=" + roles + + '}'; + } + + /** + * UserDTO Builder 클래스. + */ + public static class Builder { + private Long id; + private String username; + private String email; + private String firstName; + private String lastName; + private Integer age; + private String phoneNumber; + private String address; + private LocalDate birthDate; + private boolean active = true; + private List roles = new ArrayList<>(); + + /** + * 사용자 ID 설정. + * + * @param id 사용자 ID + * @return Builder + */ + public Builder id(Long id) { + this.id = id; + return this; + } + + /** + * 사용자명 설정 (필수). + * + * @param username 사용자명 + * @return Builder + */ + public Builder username(String username) { + this.username = username; + return this; + } + + /** + * 이메일 설정 (필수). + * + * @param email 이메일 + * @return Builder + */ + public Builder email(String email) { + this.email = email; + return this; + } + + /** + * 이름 설정. + * + * @param firstName 이름 + * @return Builder + */ + public Builder firstName(String firstName) { + this.firstName = firstName; + return this; + } + + /** + * 성 설정. + * + * @param lastName 성 + * @return Builder + */ + public Builder lastName(String lastName) { + this.lastName = lastName; + return this; + } + + /** + * 나이 설정. + * + * @param age 나이 + * @return Builder + */ + public Builder age(Integer age) { + this.age = age; + return this; + } + + /** + * 전화번호 설정. + * + * @param phoneNumber 전화번호 + * @return Builder + */ + public Builder phoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + /** + * 주소 설정. + * + * @param address 주소 + * @return Builder + */ + public Builder address(String address) { + this.address = address; + return this; + } + + /** + * 생년월일 설정. + * + * @param birthDate 생년월일 + * @return Builder + */ + public Builder birthDate(LocalDate birthDate) { + this.birthDate = birthDate; + return this; + } + + /** + * 활성화 상태 설정. + * + * @param active 활성화 여부 + * @return Builder + */ + public Builder active(boolean active) { + this.active = active; + return this; + } + + /** + * 역할 추가. + * + * @param role 역할 + * @return Builder + */ + public Builder addRole(String role) { + this.roles.add(role); + return this; + } + + /** + * 여러 역할 추가. + * + * @param roles 역할 목록 + * @return Builder + */ + public Builder roles(List roles) { + this.roles.addAll(roles); + return this; + } + + /** + * UserDTO 객체 생성. + * 필수 필드 유효성 검증 수행. + * + * @return UserDTO 객체 + * @throws IllegalStateException 필수 필드가 없는 경우 + */ + public UserDTO build() { + if (username == null || username.trim().isEmpty()) { + throw new IllegalStateException("Username is required"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalStateException("Email is required"); + } + if (!email.contains("@")) { + throw new IllegalStateException("Invalid email format"); + } + if (age != null && age < 0) { + throw new IllegalStateException("Age cannot be negative"); + } + return new UserDTO(this); + } + } +} diff --git a/builder/src/test/java/com/iluwatar/builder/EmailMessageTest.java b/builder/src/test/java/com/iluwatar/builder/EmailMessageTest.java new file mode 100644 index 000000000000..f14ba9c0238d --- /dev/null +++ b/builder/src/test/java/com/iluwatar/builder/EmailMessageTest.java @@ -0,0 +1,445 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.iluwatar.builder.EmailMessage.Priority; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * EmailMessage 테스트. + */ +class EmailMessageTest { + + @Test + void testBasicEmailCreation() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test Subject") + .body("Test Body") + .build(); + + // Then + assertNotNull(email); + assertEquals("sender@example.com", email.getFrom()); + assertEquals(1, email.getTo().size()); + assertEquals("recipient@example.com", email.getTo().get(0)); + assertEquals("Test Subject", email.getSubject()); + assertEquals("Test Body", email.getBody()); + assertEquals(Priority.NORMAL, email.getPriority()); + assertFalse(email.isHtml()); + } + + @Test + void testEmailWithMultipleRecipients() { + // Given + List recipients = Arrays.asList( + "user1@example.com", + "user2@example.com", + "user3@example.com" + ); + + // When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .toMultiple(recipients) + .subject("Test Subject") + .body("Test Body") + .build(); + + // Then + assertEquals(3, email.getTo().size()); + assertTrue(email.getTo().contains("user1@example.com")); + assertTrue(email.getTo().contains("user2@example.com")); + assertTrue(email.getTo().contains("user3@example.com")); + } + + @Test + void testEmailWithCc() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .addCc("cc1@example.com") + .addCc("cc2@example.com") + .subject("Test Subject") + .body("Test Body") + .build(); + + // Then + assertEquals(2, email.getCc().size()); + assertTrue(email.getCc().contains("cc1@example.com")); + assertTrue(email.getCc().contains("cc2@example.com")); + } + + @Test + void testEmailWithBcc() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .addBcc("bcc1@example.com") + .addBcc("bcc2@example.com") + .subject("Test Subject") + .body("Test Body") + .build(); + + // Then + assertEquals(2, email.getBcc().size()); + assertTrue(email.getBcc().contains("bcc1@example.com")); + assertTrue(email.getBcc().contains("bcc2@example.com")); + } + + @Test + void testTotalRecipientCount() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient1@example.com") + .to("recipient2@example.com") + .addCc("cc@example.com") + .addBcc("bcc@example.com") + .subject("Test Subject") + .body("Test Body") + .build(); + + // Then + assertEquals(4, email.getTotalRecipientCount()); + } + + @Test + void testHtmlEmail() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("HTML Email") + .body("

Hello

This is HTML

") + .html(true) + .build(); + + // Then + assertTrue(email.isHtml()); + } + + @Test + void testEmailPriority() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Urgent") + .body("This is urgent") + .priority(Priority.URGENT) + .build(); + + // Then + assertEquals(Priority.URGENT, email.getPriority()); + } + + @Test + void testEmailWithAttachments() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Email with attachments") + .body("Please find attachments") + .addAttachment("/path/to/file1.pdf") + .addAttachment("/path/to/file2.docx") + .build(); + + // Then + assertTrue(email.hasAttachments()); + assertEquals(2, email.getAttachments().size()); + assertTrue(email.getAttachments().contains("/path/to/file1.pdf")); + assertTrue(email.getAttachments().contains("/path/to/file2.docx")); + } + + @Test + void testEmailWithMultipleAttachments() { + // Given + List attachments = Arrays.asList( + "/path/to/file1.pdf", + "/path/to/file2.docx", + "/path/to/image.jpg" + ); + + // When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Multiple attachments") + .body("Files attached") + .attachments(attachments) + .build(); + + // Then + assertEquals(3, email.getAttachments().size()); + } + + @Test + void testEmailWithoutAttachments() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("No attachments") + .body("No files attached") + .build(); + + // Then + assertFalse(email.hasAttachments()); + assertTrue(email.getAttachments().isEmpty()); + } + + @Test + void testSendEmail() { + // Given + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test Email") + .body("Test Body") + .build(); + + // When + boolean sent = email.send(); + + // Then + assertTrue(sent); + } + + @Test + void testCreatedAt() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test") + .body("Test") + .build(); + + // Then + assertNotNull(email.getCreatedAt()); + } + + @Test + void testMissingFromThrowsException() { + // Given + EmailMessage.Builder builder = EmailMessage.builder() + .to("recipient@example.com") + .subject("Test") + .body("Test"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testInvalidFromEmailThrowsException() { + // Given + EmailMessage.Builder builder = EmailMessage.builder() + .from("invalid-email") + .to("recipient@example.com") + .subject("Test") + .body("Test"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testMissingToThrowsException() { + // Given + EmailMessage.Builder builder = EmailMessage.builder() + .from("sender@example.com") + .subject("Test") + .body("Test"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testInvalidToEmailThrowsException() { + // Given + EmailMessage.Builder builder = EmailMessage.builder() + .from("sender@example.com") + .to("invalid-email") + .subject("Test") + .body("Test"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testMissingSubjectThrowsException() { + // Given + EmailMessage.Builder builder = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .body("Test"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testMissingBodyThrowsException() { + // Given + EmailMessage.Builder builder = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testImmutableToList() { + // Given + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test") + .body("Test") + .build(); + + // When & Then + assertThrows(UnsupportedOperationException.class, () -> { + email.getTo().add("new@example.com"); + }); + } + + @Test + void testImmutableCcList() { + // Given + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .addCc("cc@example.com") + .subject("Test") + .body("Test") + .build(); + + // When & Then + assertThrows(UnsupportedOperationException.class, () -> { + email.getCc().add("new@example.com"); + }); + } + + @Test + void testImmutableBccList() { + // Given + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .addBcc("bcc@example.com") + .subject("Test") + .body("Test") + .build(); + + // When & Then + assertThrows(UnsupportedOperationException.class, () -> { + email.getBcc().add("new@example.com"); + }); + } + + @Test + void testImmutableAttachments() { + // Given + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test") + .body("Test") + .addAttachment("/path/to/file.pdf") + .build(); + + // When & Then + assertThrows(UnsupportedOperationException.class, () -> { + email.getAttachments().add("/new/file.pdf"); + }); + } + + @Test + void testToString() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test Email") + .body("Test Body") + .priority(Priority.HIGH) + .build(); + + String result = email.toString(); + + // Then + assertTrue(result.contains("sender@example.com")); + assertTrue(result.contains("Test Email")); + assertTrue(result.contains("HIGH")); + } + + @Test + void testCompleteEmail() { + // Given & When + EmailMessage email = EmailMessage.builder() + .from("sender@example.com") + .to("recipient1@example.com") + .to("recipient2@example.com") + .addCc("cc@example.com") + .addBcc("bcc@example.com") + .subject("Complete Email") + .body("

Welcome

") + .html(true) + .priority(Priority.HIGH) + .addAttachment("/path/to/document.pdf") + .build(); + + // Then + assertNotNull(email); + assertEquals("sender@example.com", email.getFrom()); + assertEquals(2, email.getTo().size()); + assertEquals(1, email.getCc().size()); + assertEquals(1, email.getBcc().size()); + assertEquals("Complete Email", email.getSubject()); + assertTrue(email.isHtml()); + assertEquals(Priority.HIGH, email.getPriority()); + assertTrue(email.hasAttachments()); + assertEquals(4, email.getTotalRecipientCount()); + } +} diff --git a/builder/src/test/java/com/iluwatar/builder/HttpRequestTest.java b/builder/src/test/java/com/iluwatar/builder/HttpRequestTest.java new file mode 100644 index 000000000000..bd54b59dbadb --- /dev/null +++ b/builder/src/test/java/com/iluwatar/builder/HttpRequestTest.java @@ -0,0 +1,351 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.iluwatar.builder.HttpRequest.HttpMethod; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * HttpRequest 테스트. + */ +class HttpRequestTest { + + @Test + void testBasicGetRequest() { + // Given & When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .build(); + + // Then + assertNotNull(request); + assertEquals(HttpMethod.GET, request.getMethod()); + assertEquals("https://api.example.com/users", request.getUrl()); + assertEquals(30000, request.getTimeout()); + assertTrue(request.isFollowRedirects()); + } + + @Test + void testPostRequest() { + // Given & When + HttpRequest request = HttpRequest.builder() + .post() + .url("https://api.example.com/users") + .jsonBody("{\"name\": \"John Doe\"}") + .build(); + + // Then + assertEquals(HttpMethod.POST, request.getMethod()); + assertEquals("{\"name\": \"John Doe\"}", request.getBody()); + assertEquals("application/json", request.getHeaders().get("Content-Type")); + } + + @Test + void testPutRequest() { + // Given & When + HttpRequest request = HttpRequest.builder() + .put() + .url("https://api.example.com/users/1") + .body("updated data") + .build(); + + // Then + assertEquals(HttpMethod.PUT, request.getMethod()); + } + + @Test + void testDeleteRequest() { + // Given & When + HttpRequest request = HttpRequest.builder() + .delete() + .url("https://api.example.com/users/1") + .build(); + + // Then + assertEquals(HttpMethod.DELETE, request.getMethod()); + } + + @Test + void testHeaders() { + // Given & When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .addHeader("Authorization", "Bearer token123") + .addHeader("Accept", "application/json") + .build(); + + // Then + assertEquals(2, request.getHeaders().size()); + assertEquals("Bearer token123", request.getHeaders().get("Authorization")); + assertEquals("application/json", request.getHeaders().get("Accept")); + } + + @Test + void testMultipleHeaders() { + // Given + Map headers = new HashMap<>(); + headers.put("X-Custom-Header-1", "value1"); + headers.put("X-Custom-Header-2", "value2"); + + // When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .headers(headers) + .build(); + + // Then + assertTrue(request.getHeaders().containsKey("X-Custom-Header-1")); + assertTrue(request.getHeaders().containsKey("X-Custom-Header-2")); + } + + @Test + void testQueryParams() { + // Given & When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .addQueryParam("page", "1") + .addQueryParam("size", "10") + .build(); + + // Then + assertEquals(2, request.getQueryParams().size()); + assertEquals("1", request.getQueryParams().get("page")); + assertEquals("10", request.getQueryParams().get("size")); + } + + @Test + void testFullUrlWithQueryParams() { + // Given & When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .addQueryParam("page", "1") + .addQueryParam("size", "10") + .build(); + + // Then + String fullUrl = request.getFullUrl(); + assertTrue(fullUrl.contains("page=1")); + assertTrue(fullUrl.contains("size=10")); + assertTrue(fullUrl.contains("?")); + } + + @Test + void testFullUrlWithoutQueryParams() { + // Given & When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .build(); + + // Then + assertEquals("https://api.example.com/users", request.getFullUrl()); + } + + @Test + void testTimeout() { + // Given & When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .timeout(5000) + .build(); + + // Then + assertEquals(5000, request.getTimeout()); + } + + @Test + void testFollowRedirects() { + // Given & When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .followRedirects(false) + .build(); + + // Then + assertFalse(request.isFollowRedirects()); + } + + @Test + void testJsonBody() { + // Given + String json = "{\"username\": \"john\", \"email\": \"john@example.com\"}"; + + // When + HttpRequest request = HttpRequest.builder() + .post() + .url("https://api.example.com/users") + .jsonBody(json) + .build(); + + // Then + assertEquals(json, request.getBody()); + assertEquals("application/json", request.getHeaders().get("Content-Type")); + } + + @Test + void testExecute() { + // Given + HttpRequest request = HttpRequest.builder() + .post() + .url("https://api.example.com/users") + .addHeader("Authorization", "Bearer token") + .jsonBody("{\"name\": \"John\"}") + .build(); + + // When + String response = request.execute(); + + // Then + assertNotNull(response); + assertTrue(response.contains("Executing HTTP Request")); + assertTrue(response.contains("POST")); + assertTrue(response.contains("https://api.example.com/users")); + } + + @Test + void testMissingUrlThrowsException() { + // Given + HttpRequest.Builder builder = HttpRequest.builder(); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testInvalidUrlThrowsException() { + // Given + HttpRequest.Builder builder = HttpRequest.builder() + .url("invalid-url"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testNegativeTimeoutThrowsException() { + // Given + HttpRequest.Builder builder = HttpRequest.builder() + .url("https://api.example.com/users") + .timeout(-1000); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testImmutableHeaders() { + // Given + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .addHeader("Authorization", "Bearer token") + .build(); + + // When & Then + assertThrows(UnsupportedOperationException.class, () -> { + request.getHeaders().put("New-Header", "value"); + }); + } + + @Test + void testImmutableQueryParams() { + // Given + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .addQueryParam("page", "1") + .build(); + + // When & Then + assertThrows(UnsupportedOperationException.class, () -> { + request.getQueryParams().put("size", "10"); + }); + } + + @Test + void testToString() { + // Given & When + HttpRequest request = HttpRequest.builder() + .post() + .url("https://api.example.com/users") + .addHeader("Authorization", "Bearer token") + .jsonBody("{\"name\": \"John\"}") + .build(); + + String result = request.toString(); + + // Then + assertTrue(result.contains("POST")); + assertTrue(result.contains("https://api.example.com/users")); + } + + @Test + void testConvenienceMethodGet() { + // Given & When + HttpRequest request = HttpRequest.builder() + .get() + .url("https://api.example.com/users") + .build(); + + // Then + assertEquals(HttpMethod.GET, request.getMethod()); + } + + @Test + void testMultipleQueryParams() { + // Given + Map params = new HashMap<>(); + params.put("filter", "active"); + params.put("sort", "name"); + + // When + HttpRequest request = HttpRequest.builder() + .url("https://api.example.com/users") + .queryParams(params) + .build(); + + // Then + assertEquals(2, request.getQueryParams().size()); + assertEquals("active", request.getQueryParams().get("filter")); + assertEquals("sort", request.getQueryParams().get("sort")); + } + + @Test + void testGetRequestWithoutBody() { + // Given & When + HttpRequest request = HttpRequest.builder() + .get() + .url("https://api.example.com/users") + .build(); + + // Then + assertNull(request.getBody()); + } +} diff --git a/builder/src/test/java/com/iluwatar/builder/UserDTOTest.java b/builder/src/test/java/com/iluwatar/builder/UserDTOTest.java new file mode 100644 index 000000000000..6892db1c2232 --- /dev/null +++ b/builder/src/test/java/com/iluwatar/builder/UserDTOTest.java @@ -0,0 +1,279 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * UserDTO 테스트. + */ +class UserDTOTest { + + @Test + void testBasicUserCreation() { + // Given & When + UserDTO user = UserDTO.builder() + .username("john.doe") + .email("john@example.com") + .build(); + + // Then + assertNotNull(user); + assertEquals("john.doe", user.getUsername()); + assertEquals("john@example.com", user.getEmail()); + assertTrue(user.isActive()); // 기본값 + } + + @Test + void testFullUserCreation() { + // Given + LocalDate birthDate = LocalDate.of(1990, 1, 15); + + // When + UserDTO user = UserDTO.builder() + .id(1L) + .username("john.doe") + .email("john@example.com") + .firstName("John") + .lastName("Doe") + .age(30) + .phoneNumber("010-1234-5678") + .address("123 Main St, Seoul") + .birthDate(birthDate) + .active(true) + .addRole("USER") + .addRole("ADMIN") + .build(); + + // Then + assertEquals(1L, user.getId()); + assertEquals("john.doe", user.getUsername()); + assertEquals("john@example.com", user.getEmail()); + assertEquals("John", user.getFirstName()); + assertEquals("Doe", user.getLastName()); + assertEquals("John Doe", user.getFullName()); + assertEquals(30, user.getAge()); + assertEquals("010-1234-5678", user.getPhoneNumber()); + assertEquals("123 Main St, Seoul", user.getAddress()); + assertEquals(birthDate, user.getBirthDate()); + assertTrue(user.isActive()); + assertEquals(2, user.getRoles().size()); + assertTrue(user.hasRole("USER")); + assertTrue(user.hasRole("ADMIN")); + } + + @Test + void testFullNameWithoutFirstAndLastName() { + // Given & When + UserDTO user = UserDTO.builder() + .username("john.doe") + .email("john@example.com") + .build(); + + // Then + assertEquals("john.doe", user.getFullName()); + } + + @Test + void testMultipleRoles() { + // Given + List roles = Arrays.asList("USER", "ADMIN", "MODERATOR"); + + // When + UserDTO user = UserDTO.builder() + .username("admin") + .email("admin@example.com") + .roles(roles) + .build(); + + // Then + assertEquals(3, user.getRoles().size()); + assertTrue(user.hasRole("USER")); + assertTrue(user.hasRole("ADMIN")); + assertTrue(user.hasRole("MODERATOR")); + } + + @Test + void testHasRole() { + // Given & When + UserDTO user = UserDTO.builder() + .username("john.doe") + .email("john@example.com") + .addRole("USER") + .build(); + + // Then + assertTrue(user.hasRole("USER")); + assertFalse(user.hasRole("ADMIN")); + } + + @Test + void testInactiveUser() { + // Given & When + UserDTO user = UserDTO.builder() + .username("inactive.user") + .email("inactive@example.com") + .active(false) + .build(); + + // Then + assertFalse(user.isActive()); + } + + @Test + void testMissingUsernameThrowsException() { + // Given + UserDTO.Builder builder = UserDTO.builder() + .email("test@example.com"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testEmptyUsernameThrowsException() { + // Given + UserDTO.Builder builder = UserDTO.builder() + .username("") + .email("test@example.com"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testMissingEmailThrowsException() { + // Given + UserDTO.Builder builder = UserDTO.builder() + .username("john.doe"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testInvalidEmailThrowsException() { + // Given + UserDTO.Builder builder = UserDTO.builder() + .username("john.doe") + .email("invalid-email"); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testNegativeAgeThrowsException() { + // Given + UserDTO.Builder builder = UserDTO.builder() + .username("john.doe") + .email("john@example.com") + .age(-5); + + // When & Then + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testImmutableRoles() { + // Given + UserDTO user = UserDTO.builder() + .username("john.doe") + .email("john@example.com") + .addRole("USER") + .build(); + + // When & Then + assertThrows(UnsupportedOperationException.class, () -> { + user.getRoles().add("ADMIN"); + }); + } + + @Test + void testToString() { + // Given & When + UserDTO user = UserDTO.builder() + .id(1L) + .username("john.doe") + .email("john@example.com") + .firstName("John") + .lastName("Doe") + .age(30) + .addRole("USER") + .build(); + + String result = user.toString(); + + // Then + assertTrue(result.contains("john.doe")); + assertTrue(result.contains("john@example.com")); + assertTrue(result.contains("John Doe")); + } + + @Test + void testFluentApi() { + // Given & When - Fluent API 테스트 + UserDTO user = UserDTO.builder() + .username("test") + .email("test@example.com") + .firstName("Test") + .lastName("User") + .age(25) + .phoneNumber("010-0000-0000") + .address("Test Address") + .addRole("USER") + .addRole("TESTER") + .build(); + + // Then + assertNotNull(user); + assertEquals("test", user.getUsername()); + assertEquals(2, user.getRoles().size()); + } + + @Test + void testBirthDate() { + // Given + LocalDate birthDate = LocalDate.of(1995, 5, 20); + + // When + UserDTO user = UserDTO.builder() + .username("john.doe") + .email("john@example.com") + .birthDate(birthDate) + .build(); + + // Then + assertEquals(birthDate, user.getBirthDate()); + } +} diff --git a/singleton/README.md b/singleton/README.md index b2fc0420e25a..3a5637ec5681 100644 --- a/singleton/README.md +++ b/singleton/README.md @@ -75,6 +75,103 @@ Use the Singleton pattern when * Creates tightly coupled code. The clients of the Singleton become difficult to test. * Makes it almost impossible to subclass a Singleton. +## 실무 예제 (Real-world Examples) + +### 1. ConfigurationManager - 애플리케이션 설정 관리 + +애플리케이션 전체에서 사용되는 설정값을 중앙에서 관리합니다. + +```java +// 설정값 저장 +ConfigurationManager.getInstance().setProperty("db.url", "jdbc:mysql://localhost:3306/mydb"); +ConfigurationManager.getInstance().setProperty("api.timeout", "30000"); + +// 설정값 조회 +String dbUrl = ConfigurationManager.getInstance().getProperty("db.url"); +String timeout = ConfigurationManager.getInstance().getProperty("api.timeout", "5000"); + +// 모든 설정 조회 +Map allConfig = ConfigurationManager.getInstance().getAllProperties(); +``` + +**실무 활용:** +- 데이터베이스 연결 정보 관리 +- API 엔드포인트 URL 관리 +- 환경별(dev, staging, prod) 설정 분리 +- 애플리케이션 전역 설정값 관리 + +### 2. LoggerService - 로깅 서비스 + +애플리케이션 전체에서 통일된 로깅 기능을 제공합니다. + +```java +LoggerService logger = LoggerService.getInstance(); + +// 다양한 레벨의 로그 출력 +logger.info("Application started"); +logger.debug("User input: " + userInput); +logger.warning("Memory usage above 80%"); +logger.error("Database connection failed"); +logger.error("Critical error", exception); + +// 로그 레벨 설정 +logger.setLogLevel(LogLevel.WARNING); + +// 로그 히스토리 조회 +List allLogs = logger.getLogHistory(); +List errorLogs = logger.getLogsByLevel(LogLevel.ERROR); +``` + +**실무 활용:** +- 애플리케이션 이벤트 로깅 +- 에러 및 예외 추적 +- 디버깅 정보 기록 +- 사용자 활동 모니터링 + +### 3. DatabaseConnectionPool - 데이터베이스 연결 풀 + +데이터베이스 연결을 효율적으로 관리하는 연결 풀을 제공합니다. + +```java +DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + +// 연결 획득 및 사용 +Connection conn = pool.acquireConnection(); +try { + // 데이터베이스 작업 수행 + String result = conn.executeQuery("SELECT * FROM users"); + System.out.println(result); +} finally { + // 연결 반환 (필수!) + pool.releaseConnection(conn); +} + +// 풀 상태 확인 +PoolStats stats = pool.getPoolStats(); +System.out.println("Active connections: " + stats.getActiveConnections()); +System.out.println("Available connections: " + stats.getAvailableConnections()); +``` + +**실무 활용:** +- 데이터베이스 연결 재사용으로 성능 향상 +- 동시 연결 수 제한으로 리소스 관리 +- 연결 생성/해제 오버헤드 감소 +- Thread-safe한 연결 관리 + +## 테스트 실행 + +각 실무 예제에는 comprehensive한 테스트 코드가 포함되어 있습니다: + +```bash +# 모든 테스트 실행 +mvn test + +# 특정 테스트만 실행 +mvn test -Dtest=ConfigurationManagerTest +mvn test -Dtest=LoggerServiceTest +mvn test -Dtest=DatabaseConnectionPoolTest +``` + ## Credits * [Design Patterns: Elements of Reusable Object-Oriented Software](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) diff --git a/singleton/src/main/java/com/iluwatar/singleton/ConfigurationManager.java b/singleton/src/main/java/com/iluwatar/singleton/ConfigurationManager.java new file mode 100644 index 000000000000..e9dedb08da1c --- /dev/null +++ b/singleton/src/main/java/com/iluwatar/singleton/ConfigurationManager.java @@ -0,0 +1,172 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.singleton; + +import java.util.HashMap; +import java.util.Map; + +/** + * 실무 예제: 애플리케이션 설정 관리 + * + *

ConfigurationManager는 애플리케이션 전체에서 사용되는 설정값들을 중앙에서 관리합니다. + * Singleton 패턴을 사용하여 애플리케이션 전체에서 하나의 설정 관리자만 존재하도록 보장합니다. + * + *

실무 활용 사례:

+ *
    + *
  • 데이터베이스 연결 정보 관리
  • + *
  • API 엔드포인트 URL 관리
  • + *
  • 애플리케이션 전역 설정값 관리
  • + *
  • 환경별(dev, staging, prod) 설정 분리
  • + *
+ * + *

사용 예제:

+ *
+ * // 설정값 저장
+ * ConfigurationManager.getInstance().setProperty("db.url", "jdbc:mysql://localhost:3306/mydb");
+ * ConfigurationManager.getInstance().setProperty("api.timeout", "30000");
+ *
+ * // 설정값 조회
+ * String dbUrl = ConfigurationManager.getInstance().getProperty("db.url");
+ * String timeout = ConfigurationManager.getInstance().getProperty("api.timeout");
+ * 
+ * + *

장점:

+ *
    + *
  • 전역에서 일관된 설정값 접근
  • + *
  • 메모리 효율성 (단일 인스턴스)
  • + *
  • Thread-safe한 설정 관리
  • + *
+ */ +public final class ConfigurationManager { + + private static volatile ConfigurationManager instance; + private final Map properties; + + /** + * Private 생성자로 외부에서 인스턴스 생성 방지. + */ + private ConfigurationManager() { + properties = new HashMap<>(); + // 기본 설정값 초기화 + initializeDefaultProperties(); + } + + /** + * Double-Checked Locking을 사용한 Thread-safe한 Singleton 인스턴스 반환. + * + * @return ConfigurationManager의 유일한 인스턴스 + */ + public static ConfigurationManager getInstance() { + if (instance == null) { + synchronized (ConfigurationManager.class) { + if (instance == null) { + instance = new ConfigurationManager(); + } + } + } + return instance; + } + + /** + * 기본 설정값 초기화. + */ + private void initializeDefaultProperties() { + properties.put("app.name", "Java Design Patterns"); + properties.put("app.version", "1.0.0"); + properties.put("app.environment", "development"); + properties.put("db.max.connections", "100"); + properties.put("api.timeout.ms", "30000"); + } + + /** + * 설정값 저장. + * + * @param key 설정 키 + * @param value 설정 값 + */ + public synchronized void setProperty(String key, String value) { + if (key == null || key.trim().isEmpty()) { + throw new IllegalArgumentException("Property key cannot be null or empty"); + } + properties.put(key, value); + } + + /** + * 설정값 조회. + * + * @param key 설정 키 + * @return 설정 값, 존재하지 않으면 null + */ + public String getProperty(String key) { + return properties.get(key); + } + + /** + * 기본값과 함께 설정값 조회. + * + * @param key 설정 키 + * @param defaultValue 기본값 + * @return 설정 값, 존재하지 않으면 기본값 + */ + public String getProperty(String key, String defaultValue) { + return properties.getOrDefault(key, defaultValue); + } + + /** + * 설정값 삭제. + * + * @param key 설정 키 + * @return 삭제된 값, 존재하지 않으면 null + */ + public synchronized String removeProperty(String key) { + return properties.remove(key); + } + + /** + * 모든 설정값 조회. + * + * @return 모든 설정값의 복사본 + */ + public Map getAllProperties() { + return new HashMap<>(properties); + } + + /** + * 모든 설정값 초기화. + */ + public synchronized void clearAllProperties() { + properties.clear(); + initializeDefaultProperties(); + } + + /** + * 설정값 존재 여부 확인. + * + * @param key 설정 키 + * @return 존재하면 true, 아니면 false + */ + public boolean hasProperty(String key) { + return properties.containsKey(key); + } +} diff --git a/singleton/src/main/java/com/iluwatar/singleton/DatabaseConnectionPool.java b/singleton/src/main/java/com/iluwatar/singleton/DatabaseConnectionPool.java new file mode 100644 index 000000000000..98a1cd799c5d --- /dev/null +++ b/singleton/src/main/java/com/iluwatar/singleton/DatabaseConnectionPool.java @@ -0,0 +1,309 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.singleton; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * 실무 예제: 데이터베이스 연결 풀 + * + *

DatabaseConnectionPool은 데이터베이스 연결을 효율적으로 관리하는 연결 풀을 구현합니다. + * Singleton 패턴을 사용하여 애플리케이션 전체에서 하나의 연결 풀만 존재하도록 보장합니다. + * + *

실무 활용 사례:

+ *
    + *
  • 데이터베이스 연결 재사용으로 성능 향상
  • + *
  • 동시 연결 수 제한으로 리소스 관리
  • + *
  • 연결 생성/해제 오버헤드 감소
  • + *
  • 애플리케이션 전체에서 일관된 연결 관리
  • + *
+ * + *

사용 예제:

+ *
+ * DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance();
+ *
+ * // 연결 획득
+ * Connection conn = pool.acquireConnection();
+ * try {
+ *     // 데이터베이스 작업 수행
+ *     conn.executeQuery("SELECT * FROM users");
+ * } finally {
+ *     // 연결 반환 (필수!)
+ *     pool.releaseConnection(conn);
+ * }
+ * 
+ * + *

주요 기능:

+ *
    + *
  • Thread-safe한 연결 풀 관리
  • + *
  • 최대 연결 수 제한
  • + *
  • 연결 획득 타임아웃 설정
  • + *
  • 풀 상태 모니터링
  • + *
+ */ +public final class DatabaseConnectionPool { + + private static volatile DatabaseConnectionPool instance; + private final BlockingQueue availableConnections; + private final List allConnections; + private final int maxPoolSize; + private static final int DEFAULT_POOL_SIZE = 10; + private static final int CONNECTION_TIMEOUT_SECONDS = 30; + + /** + * 데이터베이스 연결을 나타내는 내부 클래스. + * 실제 구현에서는 JDBC Connection을 사용하지만, 여기서는 시뮬레이션을 위한 간단한 구현. + */ + public static class Connection { + private final int id; + private boolean inUse; + private final long createdAt; + + public Connection(int id) { + this.id = id; + this.inUse = false; + this.createdAt = System.currentTimeMillis(); + } + + public int getId() { + return id; + } + + public boolean isInUse() { + return inUse; + } + + public void setInUse(boolean inUse) { + this.inUse = inUse; + } + + public long getCreatedAt() { + return createdAt; + } + + /** + * 쿼리 실행 시뮬레이션. + * + * @param query SQL 쿼리 + * @return 쿼리 실행 결과 (시뮬레이션) + */ + public String executeQuery(String query) { + if (!inUse) { + throw new IllegalStateException("Connection is not in use"); + } + return "Query executed on connection " + id + ": " + query; + } + + @Override + public String toString() { + return "Connection{id=" + id + ", inUse=" + inUse + "}"; + } + } + + /** + * Private 생성자로 외부에서 인스턴스 생성 방지. + * + * @param poolSize 연결 풀 크기 + */ + private DatabaseConnectionPool(int poolSize) { + this.maxPoolSize = poolSize; + this.availableConnections = new LinkedBlockingQueue<>(poolSize); + this.allConnections = new ArrayList<>(); + initializePool(); + } + + /** + * DatabaseConnectionPool의 유일한 인스턴스 반환 (기본 풀 크기). + * + * @return DatabaseConnectionPool의 유일한 인스턴스 + */ + public static DatabaseConnectionPool getInstance() { + return getInstance(DEFAULT_POOL_SIZE); + } + + /** + * DatabaseConnectionPool의 유일한 인스턴스 반환 (커스텀 풀 크기). + * Double-Checked Locking을 사용한 Thread-safe한 초기화. + * + * @param poolSize 연결 풀 크기 + * @return DatabaseConnectionPool의 유일한 인스턴스 + */ + public static DatabaseConnectionPool getInstance(int poolSize) { + if (instance == null) { + synchronized (DatabaseConnectionPool.class) { + if (instance == null) { + instance = new DatabaseConnectionPool(poolSize); + } + } + } + return instance; + } + + /** + * 연결 풀 초기화. + */ + private void initializePool() { + for (int i = 0; i < maxPoolSize; i++) { + Connection connection = new Connection(i + 1); + allConnections.add(connection); + availableConnections.offer(connection); + } + System.out.println("Connection pool initialized with " + maxPoolSize + " connections"); + } + + /** + * 연결 획득. + * 사용 가능한 연결이 없으면 타임아웃까지 대기. + * + * @return 데이터베이스 연결 + * @throws InterruptedException 대기 중 인터럽트 발생 + * @throws IllegalStateException 타임아웃 발생 + */ + public Connection acquireConnection() throws InterruptedException { + Connection connection = availableConnections.poll(CONNECTION_TIMEOUT_SECONDS, + TimeUnit.SECONDS); + + if (connection == null) { + throw new IllegalStateException("Connection pool timeout - no available connections"); + } + + synchronized (connection) { + connection.setInUse(true); + } + + System.out.println("Connection " + connection.getId() + " acquired. " + + "Available: " + getAvailableConnectionCount()); + return connection; + } + + /** + * 연결 반환. + * + * @param connection 반환할 연결 + */ + public void releaseConnection(Connection connection) { + if (connection == null) { + throw new IllegalArgumentException("Connection cannot be null"); + } + + if (!allConnections.contains(connection)) { + throw new IllegalArgumentException("Connection does not belong to this pool"); + } + + synchronized (connection) { + if (!connection.isInUse()) { + throw new IllegalStateException("Connection is not in use"); + } + connection.setInUse(false); + } + + availableConnections.offer(connection); + System.out.println("Connection " + connection.getId() + " released. " + + "Available: " + getAvailableConnectionCount()); + } + + /** + * 사용 가능한 연결 수 조회. + * + * @return 사용 가능한 연결 수 + */ + public int getAvailableConnectionCount() { + return availableConnections.size(); + } + + /** + * 사용 중인 연결 수 조회. + * + * @return 사용 중인 연결 수 + */ + public int getActiveConnectionCount() { + return maxPoolSize - availableConnections.size(); + } + + /** + * 전체 연결 풀 크기 조회. + * + * @return 전체 연결 풀 크기 + */ + public int getMaxPoolSize() { + return maxPoolSize; + } + + /** + * 연결 풀 상태 정보 조회. + * + * @return 연결 풀 상태 정보 + */ + public PoolStats getPoolStats() { + return new PoolStats( + maxPoolSize, + getActiveConnectionCount(), + getAvailableConnectionCount() + ); + } + + /** + * 연결 풀 상태 정보를 담는 클래스. + */ + public static class PoolStats { + private final int totalConnections; + private final int activeConnections; + private final int availableConnections; + + public PoolStats(int totalConnections, int activeConnections, int availableConnections) { + this.totalConnections = totalConnections; + this.activeConnections = activeConnections; + this.availableConnections = availableConnections; + } + + public int getTotalConnections() { + return totalConnections; + } + + public int getActiveConnections() { + return activeConnections; + } + + public int getAvailableConnections() { + return availableConnections; + } + + @Override + public String toString() { + return String.format("PoolStats{total=%d, active=%d, available=%d}", + totalConnections, activeConnections, availableConnections); + } + } + + /** + * 테스트를 위한 풀 리셋 (프로덕션에서는 사용하지 않음). + */ + public static void resetInstance() { + instance = null; + } +} diff --git a/singleton/src/main/java/com/iluwatar/singleton/LoggerService.java b/singleton/src/main/java/com/iluwatar/singleton/LoggerService.java new file mode 100644 index 000000000000..25205fa1d24e --- /dev/null +++ b/singleton/src/main/java/com/iluwatar/singleton/LoggerService.java @@ -0,0 +1,231 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.singleton; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 실무 예제: 로깅 서비스 + * + *

LoggerService는 애플리케이션 전체에서 통일된 로깅 기능을 제공합니다. + * Singleton 패턴을 사용하여 로그 메시지를 중앙에서 관리하고 일관된 형식으로 출력합니다. + * + *

실무 활용 사례:

+ *
    + *
  • 애플리케이션 이벤트 로깅
  • + *
  • 에러 및 예외 추적
  • + *
  • 디버깅 정보 기록
  • + *
  • 사용자 활동 모니터링
  • + *
+ * + *

사용 예제:

+ *
+ * LoggerService logger = LoggerService.getInstance();
+ * logger.info("Application started");
+ * logger.error("Database connection failed");
+ * logger.debug("User input: " + userInput);
+ * logger.warning("Memory usage above 80%");
+ * 
+ * + *

로그 레벨:

+ *
    + *
  • DEBUG: 상세한 디버깅 정보
  • + *
  • INFO: 일반적인 정보 메시지
  • + *
  • WARNING: 경고 메시지
  • + *
  • ERROR: 에러 메시지
  • + *
+ */ +public final class LoggerService { + + private static final LoggerService instance = new LoggerService(); + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final List logHistory = Collections.synchronizedList(new ArrayList<>()); + private LogLevel currentLogLevel = LogLevel.INFO; + + /** + * 로그 레벨 열거형. + */ + public enum LogLevel { + DEBUG(0), + INFO(1), + WARNING(2), + ERROR(3); + + private final int priority; + + LogLevel(int priority) { + this.priority = priority; + } + + public int getPriority() { + return priority; + } + } + + /** + * Private 생성자로 외부에서 인스턴스 생성 방지. + */ + private LoggerService() { + // Initialization on demand holder idiom을 사용하여 Thread-safe 보장 + } + + /** + * LoggerService의 유일한 인스턴스 반환. + * Initialization-on-demand holder idiom을 사용하여 Thread-safe하고 효율적인 초기화 보장. + * + * @return LoggerService의 유일한 인스턴스 + */ + public static LoggerService getInstance() { + return instance; + } + + /** + * 현재 로그 레벨 설정. + * + * @param level 설정할 로그 레벨 + */ + public void setLogLevel(LogLevel level) { + this.currentLogLevel = level; + log(LogLevel.INFO, "Log level changed to: " + level); + } + + /** + * 현재 로그 레벨 조회. + * + * @return 현재 로그 레벨 + */ + public LogLevel getLogLevel() { + return currentLogLevel; + } + + /** + * DEBUG 레벨 로그 출력. + * + * @param message 로그 메시지 + */ + public void debug(String message) { + log(LogLevel.DEBUG, message); + } + + /** + * INFO 레벨 로그 출력. + * + * @param message 로그 메시지 + */ + public void info(String message) { + log(LogLevel.INFO, message); + } + + /** + * WARNING 레벨 로그 출력. + * + * @param message 로그 메시지 + */ + public void warning(String message) { + log(LogLevel.WARNING, message); + } + + /** + * ERROR 레벨 로그 출력. + * + * @param message 로그 메시지 + */ + public void error(String message) { + log(LogLevel.ERROR, message); + } + + /** + * 예외와 함께 ERROR 레벨 로그 출력. + * + * @param message 로그 메시지 + * @param throwable 예외 객체 + */ + public void error(String message, Throwable throwable) { + String fullMessage = message + " | Exception: " + throwable.getClass().getName() + + " - " + throwable.getMessage(); + log(LogLevel.ERROR, fullMessage); + } + + /** + * 로그 메시지 기록 및 출력. + * + * @param level 로그 레벨 + * @param message 로그 메시지 + */ + private void log(LogLevel level, String message) { + if (level.getPriority() >= currentLogLevel.getPriority()) { + String timestamp = LocalDateTime.now().format(formatter); + String logEntry = String.format("[%s] [%s] %s", timestamp, level, message); + logHistory.add(logEntry); + System.out.println(logEntry); + } + } + + /** + * 로그 히스토리 조회. + * + * @return 모든 로그 메시지의 복사본 + */ + public List getLogHistory() { + return new ArrayList<>(logHistory); + } + + /** + * 특정 레벨의 로그만 필터링하여 조회. + * + * @param level 필터링할 로그 레벨 + * @return 필터링된 로그 메시지 리스트 + */ + public List getLogsByLevel(LogLevel level) { + List filtered = new ArrayList<>(); + String levelStr = "[" + level + "]"; + for (String log : logHistory) { + if (log.contains(levelStr)) { + filtered.add(log); + } + } + return filtered; + } + + /** + * 로그 히스토리 초기화. + */ + public void clearLogs() { + logHistory.clear(); + info("Log history cleared"); + } + + /** + * 로그 히스토리 개수 조회. + * + * @return 로그 메시지 개수 + */ + public int getLogCount() { + return logHistory.size(); + } +} diff --git a/singleton/src/test/java/com/iluwatar/singleton/ConfigurationManagerTest.java b/singleton/src/test/java/com/iluwatar/singleton/ConfigurationManagerTest.java new file mode 100644 index 000000000000..55ea0f04ab5c --- /dev/null +++ b/singleton/src/test/java/com/iluwatar/singleton/ConfigurationManagerTest.java @@ -0,0 +1,230 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.singleton; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * ConfigurationManager 테스트. + */ +class ConfigurationManagerTest { + + @BeforeEach + void setUp() { + // 각 테스트 전에 설정 초기화 + ConfigurationManager.getInstance().clearAllProperties(); + } + + @Test + void testSingletonInstance() { + // Given & When + ConfigurationManager instance1 = ConfigurationManager.getInstance(); + ConfigurationManager instance2 = ConfigurationManager.getInstance(); + + // Then + assertNotNull(instance1); + assertNotNull(instance2); + assertSame(instance1, instance2, "두 인스턴스는 동일해야 함"); + } + + @Test + void testDefaultProperties() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + + // When & Then + assertEquals("Java Design Patterns", config.getProperty("app.name")); + assertEquals("1.0.0", config.getProperty("app.version")); + assertEquals("development", config.getProperty("app.environment")); + assertEquals("100", config.getProperty("db.max.connections")); + assertEquals("30000", config.getProperty("api.timeout.ms")); + } + + @Test + void testSetAndGetProperty() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + String key = "test.property"; + String value = "test.value"; + + // When + config.setProperty(key, value); + + // Then + assertEquals(value, config.getProperty(key)); + } + + @Test + void testGetPropertyWithDefault() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + String nonExistentKey = "non.existent.key"; + String defaultValue = "default.value"; + + // When + String result = config.getProperty(nonExistentKey, defaultValue); + + // Then + assertEquals(defaultValue, result); + } + + @Test + void testRemoveProperty() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + String key = "test.property"; + String value = "test.value"; + config.setProperty(key, value); + + // When + String removedValue = config.removeProperty(key); + + // Then + assertEquals(value, removedValue); + assertNull(config.getProperty(key)); + } + + @Test + void testHasProperty() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + String existingKey = "app.name"; + String nonExistentKey = "non.existent.key"; + + // When & Then + assertTrue(config.hasProperty(existingKey)); + assertFalse(config.hasProperty(nonExistentKey)); + } + + @Test + void testGetAllProperties() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + config.setProperty("custom.key1", "value1"); + config.setProperty("custom.key2", "value2"); + + // When + Map allProperties = config.getAllProperties(); + + // Then + assertNotNull(allProperties); + assertTrue(allProperties.size() >= 7); // 5 기본 + 2 추가 + assertEquals("value1", allProperties.get("custom.key1")); + assertEquals("value2", allProperties.get("custom.key2")); + } + + @Test + void testClearAllProperties() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + config.setProperty("custom.key", "custom.value"); + + // When + config.clearAllProperties(); + + // Then + assertNull(config.getProperty("custom.key")); + // 기본 속성은 다시 초기화되어야 함 + assertEquals("Java Design Patterns", config.getProperty("app.name")); + } + + @Test + void testSetPropertyWithNullKey() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> { + config.setProperty(null, "value"); + }); + } + + @Test + void testSetPropertyWithEmptyKey() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> { + config.setProperty("", "value"); + }); + } + + @Test + void testSetPropertyWithWhitespaceKey() { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> { + config.setProperty(" ", "value"); + }); + } + + @Test + void testThreadSafety() throws InterruptedException { + // Given + ConfigurationManager config = ConfigurationManager.getInstance(); + int threadCount = 10; + int iterationsPerThread = 100; + Thread[] threads = new Thread[threadCount]; + + // When + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < iterationsPerThread; j++) { + String key = "thread." + threadId + ".key." + j; + String value = "value." + j; + config.setProperty(key, value); + assertEquals(value, config.getProperty(key)); + } + }); + threads[i].start(); + } + + // Wait for all threads to complete + for (Thread thread : threads) { + thread.join(); + } + + // Then - 모든 속성이 정확히 설정되었는지 확인 + for (int i = 0; i < threadCount; i++) { + for (int j = 0; j < iterationsPerThread; j++) { + String key = "thread." + i + ".key." + j; + assertEquals("value." + j, config.getProperty(key)); + } + } + } +} diff --git a/singleton/src/test/java/com/iluwatar/singleton/DatabaseConnectionPoolTest.java b/singleton/src/test/java/com/iluwatar/singleton/DatabaseConnectionPoolTest.java new file mode 100644 index 000000000000..d616b3db9772 --- /dev/null +++ b/singleton/src/test/java/com/iluwatar/singleton/DatabaseConnectionPoolTest.java @@ -0,0 +1,369 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.singleton; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.iluwatar.singleton.DatabaseConnectionPool.Connection; +import com.iluwatar.singleton.DatabaseConnectionPool.PoolStats; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * DatabaseConnectionPool 테스트. + */ +class DatabaseConnectionPoolTest { + + @BeforeEach + void setUp() { + // 각 테스트 전에 인스턴스 리셋 + DatabaseConnectionPool.resetInstance(); + } + + @AfterEach + void tearDown() { + // 테스트 후 정리 + DatabaseConnectionPool.resetInstance(); + } + + @Test + void testSingletonInstance() { + // Given & When + DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance(); + DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance(); + + // Then + assertNotNull(pool1); + assertNotNull(pool2); + assertSame(pool1, pool2, "두 인스턴스는 동일해야 함"); + } + + @Test + void testDefaultPoolSize() { + // Given & When + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + + // Then + assertEquals(10, pool.getMaxPoolSize()); + assertEquals(10, pool.getAvailableConnectionCount()); + assertEquals(0, pool.getActiveConnectionCount()); + } + + @Test + void testCustomPoolSize() { + // Given + int customSize = 5; + + // When + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(customSize); + + // Then + assertEquals(customSize, pool.getMaxPoolSize()); + assertEquals(customSize, pool.getAvailableConnectionCount()); + } + + @Test + void testAcquireConnection() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + + // When + Connection connection = pool.acquireConnection(); + + // Then + assertNotNull(connection); + assertTrue(connection.isInUse()); + assertEquals(9, pool.getAvailableConnectionCount()); + assertEquals(1, pool.getActiveConnectionCount()); + } + + @Test + void testReleaseConnection() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + Connection connection = pool.acquireConnection(); + + // When + pool.releaseConnection(connection); + + // Then + assertFalse(connection.isInUse()); + assertEquals(10, pool.getAvailableConnectionCount()); + assertEquals(0, pool.getActiveConnectionCount()); + } + + @Test + void testMultipleAcquireAndRelease() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + + // When + Connection conn1 = pool.acquireConnection(); + Connection conn2 = pool.acquireConnection(); + Connection conn3 = pool.acquireConnection(); + + // Then + assertEquals(7, pool.getAvailableConnectionCount()); + assertEquals(3, pool.getActiveConnectionCount()); + + // When + pool.releaseConnection(conn1); + pool.releaseConnection(conn2); + + // Then + assertEquals(9, pool.getAvailableConnectionCount()); + assertEquals(1, pool.getActiveConnectionCount()); + + // When + pool.releaseConnection(conn3); + + // Then + assertEquals(10, pool.getAvailableConnectionCount()); + assertEquals(0, pool.getActiveConnectionCount()); + } + + @Test + void testConnectionExecuteQuery() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + Connection connection = pool.acquireConnection(); + String query = "SELECT * FROM users"; + + // When + String result = connection.executeQuery(query); + + // Then + assertNotNull(result); + assertTrue(result.contains(query)); + assertTrue(result.contains("Connection " + connection.getId())); + + // Clean up + pool.releaseConnection(connection); + } + + @Test + void testExecuteQueryOnNonUsedConnectionThrowsException() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + Connection connection = pool.acquireConnection(); + pool.releaseConnection(connection); + + // When & Then + assertThrows(IllegalStateException.class, () -> { + connection.executeQuery("SELECT * FROM users"); + }); + } + + @Test + void testReleaseNullConnectionThrowsException() { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> { + pool.releaseConnection(null); + }); + } + + @Test + void testReleaseConnectionNotInPoolThrowsException() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + Connection externalConnection = new Connection(999); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> { + pool.releaseConnection(externalConnection); + }); + } + + @Test + void testReleaseConnectionNotInUseThrowsException() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + Connection connection = pool.acquireConnection(); + pool.releaseConnection(connection); + + // When & Then + assertThrows(IllegalStateException.class, () -> { + pool.releaseConnection(connection); + }); + } + + @Test + void testPoolStats() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(5); + + // When + Connection conn1 = pool.acquireConnection(); + Connection conn2 = pool.acquireConnection(); + PoolStats stats = pool.getPoolStats(); + + // Then + assertEquals(5, stats.getTotalConnections()); + assertEquals(2, stats.getActiveConnections()); + assertEquals(3, stats.getAvailableConnections()); + + // Clean up + pool.releaseConnection(conn1); + pool.releaseConnection(conn2); + } + + @Test + void testPoolStatsToString() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(5); + Connection conn = pool.acquireConnection(); + + // When + String statsString = pool.getPoolStats().toString(); + + // Then + assertTrue(statsString.contains("total=5")); + assertTrue(statsString.contains("active=1")); + assertTrue(statsString.contains("available=4")); + + // Clean up + pool.releaseConnection(conn); + } + + @Test + void testConnectionToString() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + Connection connection = pool.acquireConnection(); + + // When + String connectionString = connection.toString(); + + // Then + assertTrue(connectionString.contains("Connection")); + assertTrue(connectionString.contains("id=" + connection.getId())); + assertTrue(connectionString.contains("inUse=true")); + + // Clean up + pool.releaseConnection(connection); + } + + @Test + void testThreadSafetyWithMultipleThreads() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(5); + int threadCount = 10; + int iterationsPerThread = 10; + Thread[] threads = new Thread[threadCount]; + + // When + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < iterationsPerThread; j++) { + try { + Connection conn = pool.acquireConnection(); + // 연결 사용 시뮬레이션 + Thread.sleep(10); + pool.releaseConnection(conn); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + threads[i].start(); + } + + // Wait for all threads to complete + for (Thread thread : threads) { + thread.join(); + } + + // Then + assertEquals(5, pool.getAvailableConnectionCount()); + assertEquals(0, pool.getActiveConnectionCount()); + } + + @Test + void testAcquireAllConnections() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(3); + + // When + Connection conn1 = pool.acquireConnection(); + Connection conn2 = pool.acquireConnection(); + Connection conn3 = pool.acquireConnection(); + + // Then + assertEquals(0, pool.getAvailableConnectionCount()); + assertEquals(3, pool.getActiveConnectionCount()); + + // Clean up + pool.releaseConnection(conn1); + pool.releaseConnection(conn2); + pool.releaseConnection(conn3); + } + + @Test + void testConnectionCreatedTime() throws InterruptedException { + // Given + long beforeCreation = System.currentTimeMillis(); + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); + + // When + Connection connection = pool.acquireConnection(); + long afterCreation = System.currentTimeMillis(); + + // Then + assertTrue(connection.getCreatedAt() >= beforeCreation); + assertTrue(connection.getCreatedAt() <= afterCreation); + + // Clean up + pool.releaseConnection(connection); + } + + @Test + void testConnectionIdUniqueness() throws InterruptedException { + // Given + DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(5); + + // When + Connection conn1 = pool.acquireConnection(); + Connection conn2 = pool.acquireConnection(); + Connection conn3 = pool.acquireConnection(); + + // Then + assertTrue(conn1.getId() != conn2.getId()); + assertTrue(conn1.getId() != conn3.getId()); + assertTrue(conn2.getId() != conn3.getId()); + + // Clean up + pool.releaseConnection(conn1); + pool.releaseConnection(conn2); + pool.releaseConnection(conn3); + } +} diff --git a/singleton/src/test/java/com/iluwatar/singleton/LoggerServiceTest.java b/singleton/src/test/java/com/iluwatar/singleton/LoggerServiceTest.java new file mode 100644 index 000000000000..0403344cdc6f --- /dev/null +++ b/singleton/src/test/java/com/iluwatar/singleton/LoggerServiceTest.java @@ -0,0 +1,301 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.singleton; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.iluwatar.singleton.LoggerService.LogLevel; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * LoggerService 테스트. + */ +class LoggerServiceTest { + + @BeforeEach + void setUp() { + // 각 테스트 전에 로그 히스토리 초기화 + LoggerService.getInstance().clearLogs(); + LoggerService.getInstance().setLogLevel(LogLevel.INFO); + } + + @Test + void testSingletonInstance() { + // Given & When + LoggerService instance1 = LoggerService.getInstance(); + LoggerService instance2 = LoggerService.getInstance(); + + // Then + assertNotNull(instance1); + assertNotNull(instance2); + assertSame(instance1, instance2, "두 인스턴스는 동일해야 함"); + } + + @Test + void testInfoLogging() { + // Given + LoggerService logger = LoggerService.getInstance(); + String message = "Test info message"; + + // When + logger.info(message); + + // Then + List logs = logger.getLogHistory(); + assertEquals(2, logs.size()); // clearLogs에서 1개 + info 1개 + assertTrue(logs.get(1).contains("[INFO]")); + assertTrue(logs.get(1).contains(message)); + } + + @Test + void testErrorLogging() { + // Given + LoggerService logger = LoggerService.getInstance(); + String message = "Test error message"; + + // When + logger.error(message); + + // Then + List logs = logger.getLogHistory(); + assertEquals(2, logs.size()); // clearLogs에서 1개 + error 1개 + assertTrue(logs.get(1).contains("[ERROR]")); + assertTrue(logs.get(1).contains(message)); + } + + @Test + void testWarningLogging() { + // Given + LoggerService logger = LoggerService.getInstance(); + String message = "Test warning message"; + + // When + logger.warning(message); + + // Then + List logs = logger.getLogHistory(); + assertEquals(2, logs.size()); // clearLogs에서 1개 + warning 1개 + assertTrue(logs.get(1).contains("[WARNING]")); + assertTrue(logs.get(1).contains(message)); + } + + @Test + void testDebugLogging() { + // Given + LoggerService logger = LoggerService.getInstance(); + logger.setLogLevel(LogLevel.DEBUG); + String message = "Test debug message"; + + // When + logger.debug(message); + + // Then + List logs = logger.getLogHistory(); + assertTrue(logs.size() >= 2); // clearLogs + setLogLevel + debug + String lastLog = logs.get(logs.size() - 1); + assertTrue(lastLog.contains("[DEBUG]")); + assertTrue(lastLog.contains(message)); + } + + @Test + void testDebugLoggingNotShownWhenLogLevelIsInfo() { + // Given + LoggerService logger = LoggerService.getInstance(); + logger.setLogLevel(LogLevel.INFO); + int initialCount = logger.getLogCount(); + + // When + logger.debug("This should not be logged"); + + // Then + assertEquals(initialCount, logger.getLogCount(), "DEBUG 로그는 INFO 레벨에서 기록되지 않아야 함"); + } + + @Test + void testErrorLoggingWithException() { + // Given + LoggerService logger = LoggerService.getInstance(); + String message = "Error occurred"; + Exception exception = new RuntimeException("Test exception"); + + // When + logger.error(message, exception); + + // Then + List logs = logger.getLogHistory(); + String lastLog = logs.get(logs.size() - 1); + assertTrue(lastLog.contains("[ERROR]")); + assertTrue(lastLog.contains(message)); + assertTrue(lastLog.contains("RuntimeException")); + assertTrue(lastLog.contains("Test exception")); + } + + @Test + void testLogLevelFiltering() { + // Given + LoggerService logger = LoggerService.getInstance(); + logger.setLogLevel(LogLevel.WARNING); + + // When + logger.debug("Debug message"); // 기록되지 않음 + logger.info("Info message"); // 기록되지 않음 + logger.warning("Warning message"); // 기록됨 + logger.error("Error message"); // 기록됨 + + // Then + List logs = logger.getLogHistory(); + // clearLogs (INFO) + setLogLevel (INFO) + warning + error = 4 + assertTrue(logs.size() >= 2); + + List warningLogs = logger.getLogsByLevel(LogLevel.WARNING); + List errorLogs = logger.getLogsByLevel(LogLevel.ERROR); + + assertFalse(warningLogs.isEmpty()); + assertFalse(errorLogs.isEmpty()); + } + + @Test + void testGetLogsByLevel() { + // Given + LoggerService logger = LoggerService.getInstance(); + logger.setLogLevel(LogLevel.DEBUG); + + // When + logger.debug("Debug 1"); + logger.info("Info 1"); + logger.warning("Warning 1"); + logger.error("Error 1"); + logger.debug("Debug 2"); + logger.info("Info 2"); + + // Then + List debugLogs = logger.getLogsByLevel(LogLevel.DEBUG); + List infoLogs = logger.getLogsByLevel(LogLevel.INFO); + List warningLogs = logger.getLogsByLevel(LogLevel.WARNING); + List errorLogs = logger.getLogsByLevel(LogLevel.ERROR); + + assertTrue(debugLogs.size() >= 2); + assertTrue(infoLogs.size() >= 4); // clearLogs + setLogLevel + Info 1 + Info 2 + assertEquals(1, warningLogs.size()); + assertEquals(1, errorLogs.size()); + } + + @Test + void testClearLogs() { + // Given + LoggerService logger = LoggerService.getInstance(); + logger.info("Message 1"); + logger.info("Message 2"); + logger.info("Message 3"); + + // When + logger.clearLogs(); + + // Then + assertEquals(1, logger.getLogCount()); // clearLogs 자체가 로그를 생성 + assertTrue(logger.getLogHistory().get(0).contains("Log history cleared")); + } + + @Test + void testLogCount() { + // Given + LoggerService logger = LoggerService.getInstance(); + int initialCount = logger.getLogCount(); + + // When + logger.info("Message 1"); + logger.error("Message 2"); + logger.warning("Message 3"); + + // Then + assertEquals(initialCount + 3, logger.getLogCount()); + } + + @Test + void testSetAndGetLogLevel() { + // Given + LoggerService logger = LoggerService.getInstance(); + + // When + logger.setLogLevel(LogLevel.ERROR); + + // Then + assertEquals(LogLevel.ERROR, logger.getLogLevel()); + } + + @Test + void testThreadSafety() throws InterruptedException { + // Given + LoggerService logger = LoggerService.getInstance(); + int threadCount = 10; + int messagesPerThread = 50; + Thread[] threads = new Thread[threadCount]; + + // When + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < messagesPerThread; j++) { + logger.info("Thread " + threadId + " - Message " + j); + } + }); + threads[i].start(); + } + + // Wait for all threads to complete + for (Thread thread : threads) { + thread.join(); + } + + // Then + List logs = logger.getLogHistory(); + // clearLogs (1) + setLogLevel (1) + threadCount * messagesPerThread + int expectedMinimum = threadCount * messagesPerThread; + assertTrue(logs.size() >= expectedMinimum, + "Expected at least " + expectedMinimum + " logs but got " + logs.size()); + } + + @Test + void testLogFormat() { + // Given + LoggerService logger = LoggerService.getInstance(); + String message = "Test message"; + + // When + logger.info(message); + + // Then + List logs = logger.getLogHistory(); + String lastLog = logs.get(logs.size() - 1); + + // 로그 형식: [timestamp] [level] message + assertTrue(lastLog.matches("\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\] \\[INFO\\] " + message)); + } +} diff --git a/strategy/README.md b/strategy/README.md index 4c6f889e151a..8a32bc9398a3 100644 --- a/strategy/README.md +++ b/strategy/README.md @@ -27,10 +27,79 @@ Use the Strategy pattern when * An algorithm uses data that clients shouldn't know about. Use the Strategy pattern to avoid exposing complex, algorithm-specific data structures * A class defines many behaviors, and these appear as multiple conditional statements in its operations. Instead of many conditionals, move related conditional branches into their own Strategy class -## Tutorial +## Tutorial * [Strategy Pattern Tutorial](https://www.journaldev.com/1754/strategy-design-pattern-in-java-example-tutorial) +## 실무 예제 (Practical Example) + +### 결제 처리 시스템 + +전자상거래에서 다양한 결제 수단을 유연하게 처리하는 시스템입니다. + +**주요 컴포넌트:** + +1. **PaymentStrategy (인터페이스)**: 결제 전략 정의 +2. **구체적 전략들**: + - CreditCardPayment: 신용카드 결제 + - BankTransferPayment: 계좌이체 결제 + - PayPalPayment: PayPal 결제 +3. **PaymentProcessor (Context)**: 결제 처리 담당 + +**사용 예제:** + +```java +// 신용카드로 결제 +PaymentStrategy creditCard = new CreditCardPayment( + "1234-5678-9012-3456", "John Doe", "123", "12/25" +); +PaymentProcessor processor = new PaymentProcessor(creditCard); +PaymentResult result = processor.processPayment(1000.0); + +// 런타임에 결제 방법 변경 +PaymentStrategy paypal = new PayPalPayment("user@example.com", "password"); +processor.setPaymentStrategy(paypal); +result = processor.processPayment(500.0); + +// 계좌이체로 변경 +PaymentStrategy bankTransfer = new BankTransferPayment( + "KB Bank", "1234567890", "John Doe" +); +processor.setPaymentStrategy(bankTransfer); +result = processor.processPayment(10000.0); +``` + +**실무 활용:** +- 전자상거래 결제 시스템 +- 구독 서비스 결제 +- 모바일 앱 인앱 결제 +- 다국가 결제 처리 + +**장점:** +1. **런타임 유연성**: 실행 중에 결제 방법 변경 가능 +2. **확장성**: 새로운 결제 수단 추가 용이 (OCP 준수) +3. **분리**: 결제 로직과 비즈니스 로직 분리 +4. **테스트 용이성**: 각 결제 수단별 독립적인 테스트 가능 + +**패턴 구조:** +``` +PaymentProcessor (Context) + └── uses PaymentStrategy (Interface) + ├── CreditCardPayment + ├── BankTransferPayment + └── PayPalPayment +``` + +## 테스트 실행 + +```bash +# 모든 테스트 실행 +mvn test + +# 결제 시스템 테스트만 실행 +mvn test -Dtest=PaymentProcessorTest +``` + ## Credits * [Design Patterns: Elements of Reusable Object-Oriented Software](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) diff --git a/strategy/src/main/java/com/iluwatar/strategy/payment/BankTransferPayment.java b/strategy/src/main/java/com/iluwatar/strategy/payment/BankTransferPayment.java new file mode 100644 index 000000000000..274fff613a65 --- /dev/null +++ b/strategy/src/main/java/com/iluwatar/strategy/payment/BankTransferPayment.java @@ -0,0 +1,134 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.strategy.payment; + +import java.util.UUID; + +/** + * 계좌이체 결제 전략 구현. + * + *

은행 계좌를 통한 직접 이체 결제를 처리합니다. + * 실무에서는 오픈뱅킹 API를 사용하여 실시간 계좌이체를 진행합니다. + */ +public class BankTransferPayment implements PaymentStrategy { + + private final String bankName; + private final String accountNumber; + private final String accountHolderName; + + /** + * 생성자. + * + * @param bankName 은행명 + * @param accountNumber 계좌번호 + * @param accountHolderName 예금주명 + */ + public BankTransferPayment(String bankName, String accountNumber, String accountHolderName) { + this.bankName = bankName; + this.accountNumber = accountNumber; + this.accountHolderName = accountHolderName; + } + + @Override + public PaymentResult pay(double amount) { + System.out.println("Processing bank transfer payment..."); + System.out.println("Bank: " + bankName); + System.out.println("Account: " + maskAccountNumber(accountNumber)); + System.out.println("Holder: " + accountHolderName); + System.out.println("Amount: $" + amount); + + // 계좌 유효성 검증 + if (!validateAccount()) { + return PaymentResult.failure("Invalid account information"); + } + + // 잔액 확인 시뮬레이션 + if (!checkAccountBalance(amount)) { + return PaymentResult.failure("Insufficient account balance"); + } + + // 이체 처리 시뮬레이션 + String transactionId = "BT-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + System.out.println("Transfer successful! Transaction ID: " + transactionId); + + return PaymentResult.success("Bank transfer completed", transactionId); + } + + @Override + public String getPaymentMethodName() { + return "Bank Transfer (" + bankName + ")"; + } + + @Override + public boolean canProcess(double amount) { + return amount > 0 && amount <= 100000.0; // 최대 한도 100,000 + } + + /** + * 계좌 정보 유효성 검증. + * + * @return 유효하면 true + */ + private boolean validateAccount() { + return bankName != null && !bankName.isEmpty() + && accountNumber != null && accountNumber.length() >= 10 + && accountHolderName != null && !accountHolderName.isEmpty(); + } + + /** + * 계좌 잔액 확인 (시뮬레이션). + * + * @param amount 이체 금액 + * @return 잔액이 충분하면 true + */ + private boolean checkAccountBalance(double amount) { + // 실제로는 은행 API를 통해 확인 + return amount <= 50000.0; // 시뮬레이션: 50,000까지 이체 가능 + } + + /** + * 계좌번호 마스킹. + * + * @param accountNumber 계좌번호 + * @return 마스킹된 계좌번호 + */ + private String maskAccountNumber(String accountNumber) { + if (accountNumber == null || accountNumber.length() < 4) { + return "****"; + } + return "****-****-" + accountNumber.substring(accountNumber.length() - 4); + } + + public String getBankName() { + return bankName; + } + + public String getAccountNumber() { + return accountNumber; + } + + public String getAccountHolderName() { + return accountHolderName; + } +} diff --git a/strategy/src/main/java/com/iluwatar/strategy/payment/CreditCardPayment.java b/strategy/src/main/java/com/iluwatar/strategy/payment/CreditCardPayment.java new file mode 100644 index 000000000000..b275d6462c51 --- /dev/null +++ b/strategy/src/main/java/com/iluwatar/strategy/payment/CreditCardPayment.java @@ -0,0 +1,120 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.strategy.payment; + +import java.util.UUID; + +/** + * 신용카드 결제 전략 구현. + * + *

신용카드를 통한 결제를 처리합니다. + * 실무에서는 PG사 API를 호출하여 실제 결제를 진행합니다. + */ +public class CreditCardPayment implements PaymentStrategy { + + private final String cardNumber; + private final String cardHolderName; + private final String cvv; + private final String expiryDate; + + /** + * 생성자. + * + * @param cardNumber 카드 번호 + * @param cardHolderName 카드 소유자명 + * @param cvv CVV 번호 + * @param expiryDate 만료일 (MM/YY) + */ + public CreditCardPayment(String cardNumber, String cardHolderName, + String cvv, String expiryDate) { + this.cardNumber = cardNumber; + this.cardHolderName = cardHolderName; + this.cvv = cvv; + this.expiryDate = expiryDate; + } + + @Override + public PaymentResult pay(double amount) { + System.out.println("Processing credit card payment..."); + System.out.println("Card: **** **** **** " + cardNumber.substring(cardNumber.length() - 4)); + System.out.println("Amount: $" + amount); + + // 카드 유효성 검증 + if (!validateCard()) { + return PaymentResult.failure("Invalid card information"); + } + + // 잔액 확인 시뮬레이션 + if (!checkBalance(amount)) { + return PaymentResult.failure("Insufficient credit limit"); + } + + // 결제 처리 시뮬레이션 + String transactionId = "CC-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + System.out.println("Payment successful! Transaction ID: " + transactionId); + + return PaymentResult.success("Credit card payment completed", transactionId); + } + + @Override + public String getPaymentMethodName() { + return "Credit Card"; + } + + @Override + public boolean canProcess(double amount) { + return amount > 0 && amount <= 10000.0; // 최대 한도 10,000 + } + + /** + * 카드 정보 유효성 검증. + * + * @return 유효하면 true + */ + private boolean validateCard() { + // 간단한 검증 로직 + return cardNumber != null && cardNumber.length() >= 13 + && cvv != null && cvv.length() == 3 + && expiryDate != null && expiryDate.matches("\\d{2}/\\d{2}"); + } + + /** + * 잔액 확인 (시뮬레이션). + * + * @param amount 결제 금액 + * @return 잔액이 충분하면 true + */ + private boolean checkBalance(double amount) { + // 실제로는 카드사 API를 통해 확인 + return amount <= 5000.0; // 시뮬레이션: 5000까지 사용 가능 + } + + public String getCardNumber() { + return cardNumber; + } + + public String getCardHolderName() { + return cardHolderName; + } +} diff --git a/strategy/src/main/java/com/iluwatar/strategy/payment/PayPalPayment.java b/strategy/src/main/java/com/iluwatar/strategy/payment/PayPalPayment.java new file mode 100644 index 000000000000..6a611b4d130f --- /dev/null +++ b/strategy/src/main/java/com/iluwatar/strategy/payment/PayPalPayment.java @@ -0,0 +1,124 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.strategy.payment; + +import java.util.UUID; + +/** + * PayPal 결제 전략 구현. + * + *

PayPal 계정을 통한 결제를 처리합니다. + * 실무에서는 PayPal API를 사용하여 OAuth 인증 후 결제를 진행합니다. + */ +public class PayPalPayment implements PaymentStrategy { + + private final String email; + private final String password; + + /** + * 생성자. + * + * @param email PayPal 이메일 + * @param password PayPal 비밀번호 + */ + public PayPalPayment(String email, String password) { + this.email = email; + this.password = password; + } + + @Override + public PaymentResult pay(double amount) { + System.out.println("Processing PayPal payment..."); + System.out.println("Email: " + maskEmail(email)); + System.out.println("Amount: $" + amount); + + // 로그인 검증 + if (!authenticate()) { + return PaymentResult.failure("PayPal authentication failed"); + } + + // 잔액 확인 시뮬레이션 + if (!checkPayPalBalance(amount)) { + return PaymentResult.failure("Insufficient PayPal balance"); + } + + // 결제 처리 시뮬레이션 + String transactionId = "PP-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + System.out.println("PayPal payment successful! Transaction ID: " + transactionId); + + return PaymentResult.success("PayPal payment completed", transactionId); + } + + @Override + public String getPaymentMethodName() { + return "PayPal"; + } + + @Override + public boolean canProcess(double amount) { + return amount > 0 && amount <= 25000.0; // 최대 한도 25,000 + } + + /** + * PayPal 인증 (시뮬레이션). + * + * @return 인증 성공하면 true + */ + private boolean authenticate() { + // 실제로는 PayPal OAuth API를 사용 + return email != null && email.contains("@") + && password != null && password.length() >= 6; + } + + /** + * PayPal 잔액 확인 (시뮬레이션). + * + * @param amount 결제 금액 + * @return 잔액이 충분하면 true + */ + private boolean checkPayPalBalance(double amount) { + // 실제로는 PayPal API를 통해 확인 + return amount <= 10000.0; // 시뮬레이션: 10,000까지 사용 가능 + } + + /** + * 이메일 마스킹. + * + * @param email 이메일 + * @return 마스킹된 이메일 + */ + private String maskEmail(String email) { + if (email == null || !email.contains("@")) { + return "***@***.com"; + } + String[] parts = email.split("@"); + String username = parts[0]; + String masked = username.substring(0, Math.min(3, username.length())) + "***"; + return masked + "@" + parts[1]; + } + + public String getEmail() { + return email; + } +} diff --git a/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentProcessor.java b/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentProcessor.java new file mode 100644 index 000000000000..c1bee493e2d7 --- /dev/null +++ b/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentProcessor.java @@ -0,0 +1,150 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.strategy.payment; + +/** + * 실무 예제: 결제 처리 시스템 (Strategy Pattern Context). + * + *

PaymentProcessor는 다양한 결제 전략을 사용하여 결제를 처리합니다. + * 런타임에 결제 방법을 변경할 수 있어 유연한 결제 시스템을 구현할 수 있습니다. + * + *

실무 활용 사례:

+ *
    + *
  • 전자상거래 결제 시스템
  • + *
  • 구독 서비스 결제
  • + *
  • 모바일 앱 인앱 결제
  • + *
  • 다국가 결제 처리
  • + *
+ * + *

사용 예제:

+ *
+ * // 신용카드 결제
+ * PaymentStrategy creditCard = new CreditCardPayment(
+ *     "1234-5678-9012-3456", "John Doe", "123", "12/25"
+ * );
+ * PaymentProcessor processor = new PaymentProcessor(creditCard);
+ * PaymentResult result = processor.processPayment(100.0);
+ *
+ * // 런타임에 결제 방법 변경
+ * processor.setPaymentStrategy(new PayPalPayment("user@example.com", "password"));
+ * result = processor.processPayment(200.0);
+ * 
+ * + *

장점:

+ *
    + *
  • 런타임에 결제 방법 변경 가능
  • + *
  • 새로운 결제 수단 추가 용이 (OCP 준수)
  • + *
  • 결제 로직과 비즈니스 로직 분리
  • + *
  • 각 결제 수단별 독립적인 테스트 가능
  • + *
+ */ +public class PaymentProcessor { + + private PaymentStrategy paymentStrategy; + + /** + * 생성자. + * + * @param paymentStrategy 결제 전략 + */ + public PaymentProcessor(PaymentStrategy paymentStrategy) { + this.paymentStrategy = paymentStrategy; + } + + /** + * 결제 전략 설정. + * 런타임에 결제 방법을 변경할 수 있습니다. + * + * @param paymentStrategy 새로운 결제 전략 + */ + public void setPaymentStrategy(PaymentStrategy paymentStrategy) { + this.paymentStrategy = paymentStrategy; + } + + /** + * 현재 결제 전략 조회. + * + * @return 현재 결제 전략 + */ + public PaymentStrategy getPaymentStrategy() { + return paymentStrategy; + } + + /** + * 결제 처리. + * + * @param amount 결제 금액 + * @return 결제 결과 + */ + public PaymentResult processPayment(double amount) { + if (paymentStrategy == null) { + return PaymentResult.failure("No payment method selected"); + } + + if (amount <= 0) { + return PaymentResult.failure("Invalid payment amount"); + } + + if (!paymentStrategy.canProcess(amount)) { + return PaymentResult.failure( + "Amount exceeds limit for " + paymentStrategy.getPaymentMethodName() + ); + } + + System.out.println("========================================"); + System.out.println("Starting payment with: " + paymentStrategy.getPaymentMethodName()); + System.out.println("========================================"); + + PaymentResult result = paymentStrategy.pay(amount); + + System.out.println("========================================"); + System.out.println("Payment " + (result.isSuccess() ? "SUCCESS" : "FAILED")); + System.out.println("Message: " + result.getMessage()); + if (result.getTransactionId() != null) { + System.out.println("Transaction ID: " + result.getTransactionId()); + } + System.out.println("========================================\n"); + + return result; + } + + /** + * 결제 가능 여부 확인. + * + * @param amount 결제 금액 + * @return 결제 가능하면 true + */ + public boolean canProcessPayment(double amount) { + return paymentStrategy != null && paymentStrategy.canProcess(amount); + } + + /** + * 현재 선택된 결제 수단 이름 조회. + * + * @return 결제 수단 이름 + */ + public String getCurrentPaymentMethod() { + return paymentStrategy != null ? paymentStrategy.getPaymentMethodName() : "None"; + } +} diff --git a/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentResult.java b/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentResult.java new file mode 100644 index 000000000000..99333588f98a --- /dev/null +++ b/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentResult.java @@ -0,0 +1,98 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.strategy.payment; + +import java.time.LocalDateTime; + +/** + * 결제 결과를 나타내는 클래스. + */ +public class PaymentResult { + + private final boolean success; + private final String message; + private final String transactionId; + private final LocalDateTime timestamp; + + /** + * 생성자. + * + * @param success 결제 성공 여부 + * @param message 결과 메시지 + * @param transactionId 거래 ID + */ + public PaymentResult(boolean success, String message, String transactionId) { + this.success = success; + this.message = message; + this.transactionId = transactionId; + this.timestamp = LocalDateTime.now(); + } + + /** + * 성공한 결제 결과 생성. + * + * @param message 성공 메시지 + * @param transactionId 거래 ID + * @return 성공 결과 + */ + public static PaymentResult success(String message, String transactionId) { + return new PaymentResult(true, message, transactionId); + } + + /** + * 실패한 결제 결과 생성. + * + * @param message 실패 메시지 + * @return 실패 결과 + */ + public static PaymentResult failure(String message) { + return new PaymentResult(false, message, null); + } + + public boolean isSuccess() { + return success; + } + + public String getMessage() { + return message; + } + + public String getTransactionId() { + return transactionId; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return "PaymentResult{" + + "success=" + success + + ", message='" + message + '\'' + + ", transactionId='" + transactionId + '\'' + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentStrategy.java b/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentStrategy.java new file mode 100644 index 000000000000..b657f39a9592 --- /dev/null +++ b/strategy/src/main/java/com/iluwatar/strategy/payment/PaymentStrategy.java @@ -0,0 +1,56 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.strategy.payment; + +/** + * 결제 전략 인터페이스. + * + *

Strategy 패턴의 핵심 인터페이스로, 다양한 결제 방법을 추상화합니다. + * 각 결제 수단은 이 인터페이스를 구현하여 동일한 방식으로 처리될 수 있습니다. + */ +public interface PaymentStrategy { + + /** + * 결제 처리. + * + * @param amount 결제 금액 + * @return 결제 성공 여부 + */ + PaymentResult pay(double amount); + + /** + * 결제 수단 이름 조회. + * + * @return 결제 수단 이름 + */ + String getPaymentMethodName(); + + /** + * 결제 가능 여부 확인. + * + * @param amount 결제 금액 + * @return 결제 가능하면 true + */ + boolean canProcess(double amount); +} diff --git a/strategy/src/test/java/com/iluwatar/strategy/payment/PaymentProcessorTest.java b/strategy/src/test/java/com/iluwatar/strategy/payment/PaymentProcessorTest.java new file mode 100644 index 000000000000..f1ebe32f19d3 --- /dev/null +++ b/strategy/src/test/java/com/iluwatar/strategy/payment/PaymentProcessorTest.java @@ -0,0 +1,208 @@ +/* + * The MIT License + * Copyright © 2014-2021 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.strategy.payment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * PaymentProcessor 테스트. + */ +class PaymentProcessorTest { + + @Test + void testCreditCardPaymentSuccess() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentProcessor processor = new PaymentProcessor(creditCard); + + // When + PaymentResult result = processor.processPayment(1000.0); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getTransactionId()); + assertTrue(result.getTransactionId().startsWith("CC-")); + } + + @Test + void testCreditCardPaymentInsufficientLimit() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentProcessor processor = new PaymentProcessor(creditCard); + + // When + PaymentResult result = processor.processPayment(6000.0); + + // Then + assertFalse(result.isSuccess()); + assertEquals("Insufficient credit limit", result.getMessage()); + } + + @Test + void testBankTransferPaymentSuccess() { + // Given + PaymentStrategy bankTransfer = new BankTransferPayment( + "KB Bank", "1234567890", "John Doe" + ); + PaymentProcessor processor = new PaymentProcessor(bankTransfer); + + // When + PaymentResult result = processor.processPayment(10000.0); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getTransactionId()); + assertTrue(result.getTransactionId().startsWith("BT-")); + } + + @Test + void testPayPalPaymentSuccess() { + // Given + PaymentStrategy paypal = new PayPalPayment("user@example.com", "password123"); + PaymentProcessor processor = new PaymentProcessor(paypal); + + // When + PaymentResult result = processor.processPayment(500.0); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getTransactionId()); + assertTrue(result.getTransactionId().startsWith("PP-")); + } + + @Test + void testSwitchPaymentStrategy() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentProcessor processor = new PaymentProcessor(creditCard); + + // When - 신용카드로 결제 + PaymentResult result1 = processor.processPayment(1000.0); + + // Then + assertTrue(result1.isSuccess()); + assertEquals("Credit Card", processor.getCurrentPaymentMethod()); + + // When - PayPal로 변경 + PaymentStrategy paypal = new PayPalPayment("user@example.com", "password123"); + processor.setPaymentStrategy(paypal); + PaymentResult result2 = processor.processPayment(500.0); + + // Then + assertTrue(result2.isSuccess()); + assertEquals("PayPal", processor.getCurrentPaymentMethod()); + } + + @Test + void testInvalidPaymentAmount() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentProcessor processor = new PaymentProcessor(creditCard); + + // When + PaymentResult result = processor.processPayment(-100.0); + + // Then + assertFalse(result.isSuccess()); + assertEquals("Invalid payment amount", result.getMessage()); + } + + @Test + void testPaymentExceedsLimit() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentProcessor processor = new PaymentProcessor(creditCard); + + // When + PaymentResult result = processor.processPayment(15000.0); + + // Then + assertFalse(result.isSuccess()); + assertTrue(result.getMessage().contains("exceeds limit")); + } + + @Test + void testCanProcessPayment() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentProcessor processor = new PaymentProcessor(creditCard); + + // When & Then + assertTrue(processor.canProcessPayment(1000.0)); + assertTrue(processor.canProcessPayment(5000.0)); + assertFalse(processor.canProcessPayment(15000.0)); + } + + @Test + void testDifferentPaymentMethods() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentStrategy bankTransfer = new BankTransferPayment( + "KB Bank", "1234567890", "John Doe" + ); + PaymentStrategy paypal = new PayPalPayment("user@example.com", "password123"); + + // When & Then + assertEquals("Credit Card", creditCard.getPaymentMethodName()); + assertEquals("Bank Transfer (KB Bank)", bankTransfer.getPaymentMethodName()); + assertEquals("PayPal", paypal.getPaymentMethodName()); + } + + @Test + void testPaymentResultFields() { + // Given + PaymentStrategy creditCard = new CreditCardPayment( + "1234567890123456", "John Doe", "123", "12/25" + ); + PaymentProcessor processor = new PaymentProcessor(creditCard); + + // When + PaymentResult result = processor.processPayment(1000.0); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getMessage()); + assertNotNull(result.getTransactionId()); + assertNotNull(result.getTimestamp()); + } +}