티스토리 뷰
Class 내 Method 추상화(Feat. Consumer, BiConsumer, Function)
5_Clock 2024. 10. 9. 15:46서론
최근에 개발을 하면서 클래스 내에서 중복 되는 로직의 메서드가 많았던것 같다. 하지만 모든 부분이 겹치면 한 개의 메서드로 해결이 가능하지만, 같은 로직을 타다가 일부만 조금 다른 로직이거나 다른 switch 문을 타야하거나 등의 문제가 있었다. 이를 추상화 하면서 알게 된 지식에 대해 정리하고자 한다.
기존에 Consumer는 Kafka - Spring을 사용하면서만 사용했는데 이러한 중복 로직을 추상화하는데에도 쓰일 수 있다는 점을 알게 됐다.
Problem
public class ExampleClass{
public void methodN(){
// logic A
// logic B
switch(type){
// logic N
}
}
public void methodM(){
// logic A
// logic B
switch(type){
// logic M
}
}
}
위 코드를 해석하면 switch를 타기 전까진 A, B라는 같은 로직을 타고 있다.
하지만 switch문을 타고 나서는 logic이 M과 N으로 갈려 다른 기능을 수행하는 메서드가 된다.
switch문 외에도 저런 로직을 타고 가는 상황은 꽤나 많이 발생할 수 있을 것 같지만,
거의 동일한 로직을 수행하는 두 개의 메서드가 있는게 겹치는 부분이 많아 보기가 좋지 않았다.
Solution
if 문
아주 간단한 해결 방법으로는 if문을 통해서 중복 코드를 해결하는 것이다.
하지만 아래 코드에서 확인 할 수 있듯 그리 깔끔해보이는 방법도 아닐뿐더라
불필요한 flag라는 인자가 하나 추가되는 단점도 있다.
public class ExampleClass{
public void method(String flag){
// logic A
// logic B
if (flag == "N"){
switchN(type);
} else if (flag == "M"){
switchM();
}
}
private void switchN(String type){
switch(type){
// logic N
}
}
private void switchM(String type){
switch(type){
// logic M
}
}
}
그리고 추가적인 logic이 발생되면 if문이 길어지는 문제점도 있을거라고 생각한다.
Consumer 사용
사실 가장 처음 생각했던 방법은 위처럼 if문을 쓰는 것이 아닌
reflection을 사용해서 해당 method 자체를 인자로 받은 후, 실행하는 것을 생각했다.
그런데 개인적으로 reflection을 사용하기 전에 "누군가 더 쉽게 만들어 놓은게 없을까"라는 생각을 한다.
왜냐하면 reflection을 사용하게 되면서 생기는 명확한 단점이 있다고 생각하기 때문이다.
그러다 Consumer, BiConsumer를 찾게 되었다.
기존에 Kafka를 사용하면서 사용은 했지만 이런식으로 활용하는 점은 모르고 있었다.
import java.util.function.Consumer;
public class ExampleClass {
// 공통 로직을 수행하는 메서드
private void executeLogic(Consumer<String> switchLogic, String type) {
// logic A
// logic B
// 각기 다른 switch logic을 수행
switchLogic.accept(type);
}
public void methodA(String type) {
executeLogic(this::switchN, type);
}
public void methodB(String type) {
executeLogic(this::switchM, type);
}
// methodA의 switch 문 로직 (logic N)
private void switchN(String type) {
switch (type) {
// logic N
}
}
// methodB의 switch 문 로직 (logic M)
private void switchM(String type) {
switch (type) {
// logic M
}
}
}
핵심 로직을 수행하는 executeLogic이라는 메서드에서 내가 수행하는
분기가 되는 로직을 Consumer<인자의 자료형>를 통해서 받는다
해당 methodA, B를 사용하는 입장에서는 동일하게 사용이 가능하다.
사실 이 방법이 무조건 좋다고는 말하기 힘든게 우선은 구현하는 코드량이 많아졌고,
메서드를 여기저기 움직여야 한다는 점이 단점으로 생각된다.
하지만 위 예시를 보면 switch의 로직이 늘어나면 늘어날 수록 진가를 발휘 할 수 있다.
Consumer는 한개의 인자를 받은 함수를 위한 인터페이스이다.
만약 switch N, M이 2개의 인자를 필요로 한다면 BiConsumer를 사용하면 된다.
import java.util.function.Consumer;
public class Test {
public void executeLogic(BiConsumer<String, Integer> switchLogic, String type) {
// logic A
// logic B
switchLogic.accept(type);
}
public void methodB(String type) {
executeLogic(this::switchLogicM, type);
}
// methodB의 switch 문 로직 (logic M)
private void switchLogicM(String type, Integer something) {
System.out.println(something);
switch (type) {
// logic M
}
}
}
BiConsumer는 순서대로 인자의 자료형을 입력해주면 된다.
그렇다면 3개일때는 TriConsumer를 사용하면 될까?
아쉽게도 라이브러리에서는 BiConsumer까지만 지원을 한다.
하지만 Consumer를 만드는 방법은 아주 간단하다.
TriConsumer를 만들기전 Consumer, BiConsumer에 대해서 먼저 살펴보면 아래와 같다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
Objects.requireNonNull(after);
return (l, r) -> {
accept(l, r);
after.accept(l, r);
};
}
}
위를 보면 함수형 인터페이스 @FuntionalInterface를 이용해서 accept를 만들고 있다.
이러한 방법을 통해서 함수의 로직을 수행이 가능한 것이다.
그러면 동일하게 TriConsumer는 아래와 같이 만들 수 있다.
@FunctionalInterface
public interface TriConsumer<T, U, V> {
void accept(T t, U u, V v);
default TriConsumer<T, U, V> andThen(TriConsumer<? super T, ? super U, ? super V> after) {
Objects.requireNonNull(after);
return (t, u, v) -> {
accept(t, u, v);
after.accept(t, u, v);
};
}
}
주의점
위에서 Consumer나 BiConsumer를 보면 알겠지만, accept의 반환이 void이다.
따라서 특정 값을 반환해야하는 함수는 사용이 불가능 한 점이다.
그럼 특정 값을 반환하고 싶으면 어떻게 할까?
아주 간단하다 Consumer 대신 Function을 사용해주면 된다.
사용법은 위에서 설명했던 Consumer와 거의 동일하고 반환값을 마지막으로 넣어주면 된다.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// something
}
위에서 확인 가능하듯이 R이 apply 반환값의 자료형으로 되어있다.
'Programming_language > Java' 카테고리의 다른 글
[Effective Java] 4. 참조 해제 (0) | 2022.11.09 |
---|---|
[Effective Java] 3. 의존 객체 주입 (0) | 2022.11.09 |
[Effective Java] 2. 빌더 패턴 (0) | 2022.11.09 |
[Effective Java] 1. 정적 팩토리 메서드 (0) | 2022.11.07 |
- Total
- Today
- Yesterday
- logback
- centos
- frontend
- zookeeper
- API
- KAFKA
- Java
- Data Engineering
- Front
- 리액트
- Producer
- NextJS
- broker
- cs
- JPA
- Linux
- rhel
- apache
- Firebase
- feign client
- Container
- spring boot
- consumer
- docker
- 프론트엔드
- K8S
- spring
- apache kafka
- OS
- React
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |