728x170
해당 글에서는 메서드 체이닝(Method Chaining), 플루언트 인터페이스(Fluent Interface), 빌더 패턴(Builder Pattern)에 대해서 알아봅니다.
1) 메서드 체이닝(Method Chaining)
💡 메서드 체이닝(Method Chaining)
- 여러 메서드 호출을 연결하여 호출하는 프로그래밍 기술입니다. 이를 이용하면 코드를 간결하게 작성하고 가독성을 높일 수 있습니다.
- 이를 통해 객체의 메서드를 연속적으로 호출하여 작업을 수행할 수 있으며, 각 메서드는 호출된 객체를 반환하여 다음 메서드 호출이 가능하게 합니다.
- 이러한 메서드 체이닝은 플루언트 인터페이스(Fluent Interface)나 빌더 패턴(Builder Pattern)을 구현하는 데에 자주 사용되며, 간결하고 읽기 쉬운 코드를 작성하는 데 도움이 됩니다.
💡 기본 구조
- "method1", "method2", "method3" 메서드가 모두 같은 객체 "obj"에 대해 연속적으로 작업을 수행하며, 각 메서드는 연산 후에 그 결과를 반환하는 객체를 반환합니다.
obj.method1().method2().method3()
2) 플루언트 인터페이스(Fluent Interface)
💡 플루언트 인터페이스(Fluent Interface)
- 메소드 체이닝을 기반한 객체 지향 API 인터페이스 메서드입니다. 소스코드의 가독성을 높이기 위한 목적으로 사용되며 인터페이스 안에 도메인 특화 언어(DSL)를 이용하여 작성합니다.
- 각 메서드는 수정된 객체를 반환하여 다음 연산을 계속할 수 있으며, 주로 변경 가능한 객체에서 사용됩니다.
[ 더 알아보기 ]
💡 도메인 특화 언어(Domain-Specific Language, DSL)
- 특정 영역의 문제를 해결하기 위해 맞춤 제작된 컴퓨터 언어입니다. 이는 일반적인 프로그래밍 언어보다 훨씬 더 '사람에 가까운' 언어를 사용하여 특정 문제를 표현합니다. DSL은 주로 사용자 인터페이스, 데이터베이스, 웹사이트 등의 특정 영역에서 사용됩니다.
1. Stream API
💡 Stream API
- 람다식(Lambda Expression)를 이용한 기술 중에 하나로 데이터 소스(컬렉션, 배열, 난수, 파일 등…)를 조작 및 가공, 변환하여 원하는 값으로 반환해주는 인터페이스를 의미합니다.
💡 Stream API 활용 예시
- 해당 경우에서는 Optional<String> 객체를 구성하기위해 stream()을 사용하며, 해당 데이터에 값을 정제하기 위해 filter() 메서드와 findFirst() 메서드가 체이닝 되었습니다.
/*
* [CASE2] ArrayList 이용한 방법
*/
// 1. ArrayList Initialize
List<String> findFirstArrList = new ArrayList<>(Arrays.asList("kim", "lee", "park", "lee", "jung", "jin"));
// 2. ArrayList to Optional<String> : j로 시작하는 단어의 첫번째 요소를 반환합니다.
Optional<String> findFirstOptionalStr = findFirstArrList.stream().filter(i -> i.startsWith("j")).findFirst();
2. JPA Creteria API
💡 JPA Creteria API
- JPA에서 제공하는 타입 세이프한 쿼리 생성 기능입니다. 해당 API를 사용하면 JPQL(JPA Query Language)을 직접 작성하지 않고도 쿼리를 작성할 수 있으며, 컴파일 시점에 오류를 확인할 수 있어 안전합니다.
💡 JPA Creteria API 활용 예시
- 해당 예시에서는 SQL문을 메서드 체이닝을 기반으로 구성하는 Creteria API 사용예시입니다.
- 테이블과 도메인을 매핑하여 사용되며 query를 구성할때 .select() 메서드로 테이블의 컬럼을 조회하고 .where() 메서드를 통해 조건문을 생성합니다. 이는 메서드 체이닝을 통해 연결이 되어 있습니다.
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<RoomVO> query = builder.createQuery(RoomVO.class);
Root<RoomVO> tbRoom = query.from(RoomVO.class);
query
.select(tbRoom)
.where(
builder.equal(tbRoom.get("roomSq"), roomVo.getRoomSq()),
builder.equal(tbRoom.get("useYn"), true)
);
resultList = em.createQuery(query).getResultList();
3. Mockito
💡 Mockito
- 단위 테스트를 위해 모의 객체를 생성하고 관리하는 데 사용되는 Java 오픈소스 프레임워크를 의미합니다.
- 이를 사용하면 실제 객체의 동작을 모방하는 모의 객체(Mock Object)를 생성하여 코드의 ‘특정 부분을 격리’시키고 테스트하기 쉽게 만들어줍니다.
-주로 단일 컴포넌트의 동작을 테스트하는 데 사용되며 클래스 내의 개별 메서드나 함수, 서로 다른 클래스 또는 컴포넌트 간의 상호작용, 객체들 간의 협업 등을 테스트할 수 있습니다.
💡 Mockito 활용 예시
- 해당 테스트의 요점은 가상의 모의 객체를 만들어서 service에 처리된 결과를 비교하는 서버스를 테스트하기 위함입니다. 해당 처리를 위해서 메서드 체이닝 형태의 구조로 처리합니다.
1. @ExtendWith(MockitoExtension.class)
- JUnit5와 Mockito를 함께 사용하기 위한 어노테이션을 선언합니다.
2. @Mock
- 모의 객체로 사용할 sqlSession와 codeMapper를 선언합니다.
3. @InjectMocks
- 실제 테스트를 수행할 ServiceImpl로 객체를 생성하고 의존성 주입을 하기 위해 어노테이션으로 선언합니다.
4. @Test
- 실제 처리되는 테스트가 수행되는 메서드입니다.
1. 가상의 객체를 구성합니다.
2. Service와 비교할 반환값을 구성합니다.
3. Mapper에 가상의 객체를 넣고 반환되는 값을 expectedCodeList 값으로 지정합니다.
4. 가상의 sqlSession에 Mapper를 지정해 줍니다.
(* 해당 부분은 서비스 구현체(ServiceImpl)에서 사용되는 데이터를 구성하였습니다)
5. 테스트 대상 메서드를 호출합니다.
6. 반환된 코드 리스트가 예상한 값과 일치하는지 검증합니다.
/**
* CodeService 구현체를 테스트를 수행합니다.
*
* @author : lee
* @fileName : CodeServiceTest
* @since : 12/11/23
*/
@ExtendWith(MockitoExtension.class)
class CodeServiceTest {
@Mock
private SqlSession sqlSession;
@Mock
private CodeMapper codeMapper;
@InjectMocks
private CodeServiceImpl codeService;
@BeforeEach
void setUp() {
//
}
@Test
@DisplayName("객체 값을 기반으로 여러개의 코드 값들을 조회합니다.")
void selectCodeList() {
// *************** given : 사전 조건 ******************
// 1. 가상의 객체를 구성합니다.
CodeDto mockCodeDto = CodeDto.builder().cd("java").build();
// 2. Service와 비교할 반환값을 구성합니다.
List<CodeDto> expectedCodeList = new ArrayList<>();
// *************** when : 동작 검증 ******************
// 3. 가상의 codeMapper의 메서드를 호출하여 가상의 객체로 반환되도록 지정합니다.
Mockito.when(codeMapper.selectCodeList(mockCodeDto)).thenReturn(expectedCodeList);
// 4. 가상의 sqlSession에 Mapper를 지정해줍니다. (* 해당 부분은 서비스 구현체(ServiceImpl)에서 사용되는 데이터를 구성하였습니다)
Mockito.when(sqlSession.getMapper(CodeMapper.class)).thenReturn(codeMapper);
// 5. 테스트 대상 메서드를 호출합니다.
List<CodeDto> actualCodeList = codeService.selectCodeList(mockCodeDto);
// *************** when : 결과 검증 ******************
// 6. 반환된 코드 리스트가 예상한 값과 일치하는지 검증합니다.
Assertions.assertEquals(expectedCodeList, actualCodeList);
}
}
4. MockMvc
💡 MockMvc
- 스프링 프레임워크에서 제공하는 웹 애플리케이션 테스트용 라이브러리를 의미합니다. 이를 사용하면 웹 애플리케이션의 다양한 컴포넌트를 테스트할 수 있습니다.
- MockMvc를 사용하면 HTTP 요청을 작성하고 컨트롤러의 응답을 검증할 수 있습니다. 이를 통해 통합 테스트를 실행하지 않고도 컨트롤러의 동작을 확인할 수 있습니다.
💡 MockMvc 활용 예시
- MockMvc를 이용한 테스트 작업에서 플루언트 인터페이스 구조를 통하여 구성합니다.
- testExample() 메서드를 확인하면 아래와 같이 MockMvc 객체를 생성하여 이를 구성합니다. 구성은 ‘.메서드’ 형태로 메서드 체이닝 방식 구성이 되었습니다.
@ExtendWith(MockitoExtension.class) // Junit5 - Mockito 연동
class CodeControllerTest {
private MockMvc mockMvc; // HTTP 호출을 위한 MockMVC 사용
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(codeController).build();
}
@Test
public void testExample() throws Exception {
mockMvc.perform(get("/example")) // "/example"로 GET 요청 수행
.andExpect(status().isOk()) // 상태 코드 200인 성공적인 응답을 기대합니다
.andExpect(content().string("expected")) // 응답 내용이 "expected"와 같을 것으로 기대합니다
.andExpect(jsonPath("$.property").value("expected")) // JSON 속성 "property"의 값이 "expected"와 같을 것으로 기대합니다
.andExpect(view().name("expectedView")) // 뷰 이름이 "expectedView"와 같을 것으로 기대합니다
.andExpect(model().attribute("attributeName", "expectedValue")) // 모델 속성 "attributeName"의 값이 "expectedValue"와 같을 것으로 기대합니다
.andExpect(redirectedUrl("expectedUrl")); // "expectedUrl"로의 리다이렉트를 기대합니다
}
}
3) 빌더 패턴(Builder Pattern)
💡 빌더 패턴(Builder Pattern)
- 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴을 의미합니다.
- 생성자의 매개 변수가 많을 때 객체 생성을 더 쉽고 가독성 있게 만드는 디자인 패턴으로 불변성을 가진 객체를 만들때 특히 유용합니다.
💡 사용예시
- 아래의 코드에서는 객체의 생성 과정을 구성합니다.
import lombok.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SuccessCode {
private int status;
private String code;
private String message;
@Builder
SuccessCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
💡 사용예시
- 아래의 코드에서는 다양한 객체의 표현 방법을 구성합니다.
- 기존의 인스턴스를 생성하고 setter의 사용하던 방식이 아닌 builder를 이용하여 객체를 구성합니다.
// Builder Pattern 사용 -1
SuccessCode successCode = SuccessCode.builder()
.status(200)
.message("성공 되었습니다.")
.code("200")
.build();
// Builder Pattern 사용 -2
SuccessCode successCode2 = SuccessCode.builder()
.status(200)
.build();
// Builder Pattern 사용 -3
SuccessCode successCode3 = SuccessCode.builder()
.message("성공 되었습니다.")
.build();
// Builder Pattern 사용 -4
SuccessCode successCode4 = SuccessCode.builder()
.code("200")
.build();
[더 알아보기]
💡 빌더 패턴의 경우 메서드 체이닝을 사용하는것이 맞는건가?
- 빌더 패턴을 사용할 때 메서드 체이닝 기법이 일반적으로 사용됩니다. 이는 각 메서드가 객체 자신을 반환하도록 함으로써 여러 메서드 호출을 연결 할 수 있게 합니다.
- 이 방식은 코드의 가독성을 향상시키고 객체 생성을 더 유연하게 만듭니다.
1. Lombok Builder
💡 Lombok Builder
- Java에서 생성자 패턴의 종류 중 하나로 사용되는 Builder 패턴은 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴을 의미합니다.
- 이러한 생성자를 구성하여 사용할 때 메서드 체이닝으로 사용이 됩니다.
💡 사용예시 -1 : Lombok Builder 선언 및 활용 방법
- Builder 패턴 구성하고 이를 사용할 때 메서드 체이닝이 사용되는 예시입니다.
- Builder 패턴에서는 객체를 구성할때 .builder()로 시작하여 .build()로 닫습니다. 이 사이에 해당 값을 채워넣기 위해 .status(), .message(), .code()와 같은 형태의 메서드를 통하여 해당 값들을 채워 객체를 구성합니다.
import lombok.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SuccessCode {
private int status;
private String code;
private String message;
@Builder
SuccessCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
// Builder Pattern 사용
SuccessCode successCode = SuccessCode.builder()
.status(200)
.message("성공 되었습니다.")
.code("200")
.build();
💡 사용예시 -2 : Lombok Builder로 구성된 형태를 호출하여 값을 재구성
- 해당 경우에서는 위에서 객체로 값을 구성한 형태라면 이를 특정 값에 대해서만 재구성하기 위한 방법으로 사용되는 경우입니다.
- 우선 userDto1라는 객체를 구성하였고 객체에 값을 채웠습니다. 이전의 값을 유지한채 일부 값만 변경하고자 하는 경우는 .toBuilder() 메서드를 이용하여 일부만 수정합니다ㅍ
**
* 객체1을 구성합니다.
*/
UserDto userDto1 = UserDto
.userBuilder()
.userSq(1)
.userNm("Lee")
.userId("adjh54")
.build();
log.debug(userDto1.toString()); // UserDto(userSq=1, userId=adjh54, userPw=null, userNm=Lee, userSt=null)
/**
* 구성한 객체1을 기반으로 객체2를 재구성합니다.
* 객체1의 값은 유지하되 변경하고자 하는 값은 객체2로 변경합니다.
*/
UserDto userDto2 = userDto1
.toBuilder()
.userNm("Jong")
.build();
log.debug(userDto2.toString()); // UserDto(userSq=1, userId=adjh54, userPw=null, userNm=Jong, userSt=null)
[더 알아보기]
💡 Builder Pattern에서 toBuilder를 이용하면 불변성 객체가 아니지 않은가?
- toBuilder 메소드를 사용하면 원래 객체를 복사하여 새로운 Builder 객체를 생성합니다. 이렇게 생성된 Builder를 이용해 원래 객체의 일부 값을 변경한 상태로 새 객체를 생성할 수 있습니다.
- 이 경우, 원래 객체는 불변성을 유지하며 새로운 객체는 원래 객체의 사본이 되는 것입니다. 따라서, toBuilder를 사용하더라도 Builder 패턴의 불변성은 유지됩니다.
4) 플루언트 인터페이스(Fluent Interface) vs 빌더 패턴(Builder Pattern)
💡 플루언트 인터페이스(Fluent Interface) vs 빌더 패턴(Builder Pattern)
- 플루언트 인터페이스와 빌더 패턴은 둘 다 객체 생성을 쉽고 명확하게 만들어주는 디자인 패턴입니다. 둘 다 메서드 체인을 사용하여 가독성을 높이고 코드를 간결하게 만듭니다.
분류 | 플루언트 인터페이스 | 빌더 패턴 |
메서드 체이닝 사용 | O | O |
복잡한 객체 생성 | X | O |
객체 상태 변경 | O | X |
불변성 유지 | X | O |
1. 불변성에 대한 예시
💡 플루언트 인터페이스(Fluent Interface)
- 해당 경우 setMake(), setModel(), setColor()을 통해 메서드 각각의 자신 상태를 변경하고 변경된 자신을 반환하는 형태로 불변성이 존재하지 않습니다.
Car car = new Car();
car.setMake("Toyota")
.setModel("Corolla")
.setColor("Blue");
💡 빌더 패턴(Builder Pattern)
- 해당 경우 각 단계에서 자신을 변경하지 않고 새로운 빌더(CarBuilder() ~ build())를 최종 반환합니다. 이렇게 하면 최종 객체가 생성될 때까지 원본 빌더 객체는 변환되지 않습니다.
Car car = new CarBuilder()
.withMake("Toyota")
.withModel("Corolla")
.withColor("Blue")
.build();
💡 참고 문서
Difference Between Fluent Interface and Builder Pattern in Java | Baeldung
오늘도 감사합니다. 😀
그리드형
'Java > 아키텍처 & 디자인 패턴' 카테고리의 다른 글
[Java] 람다식(Lambda Expression), 함수형 인터페이스(Functional Interface) 이해하기 (1) | 2023.12.05 |
---|---|
[Java/디자인패턴] 싱글턴 패턴(Signleton Pattern) 이해하기 -1 : 정의 및 종류 (1) | 2023.12.02 |
[Java] Spring Framework 주요 특징 이해하기 : DI, IoC, POJO, AOP (1) | 2023.10.28 |
[Java] MacOS 환경에서 Java JDK 설정 및 변경하기 : homebrew, 다운로드 파일 (2) | 2023.06.28 |
[Java] Gradle 버전 확인 및 변경 방법 (2) | 2023.06.27 |