

public class Client {
public static void main(String[] args) {
FileProcessor fileProcessor = new FileProcessor("number.txt");
int result = fileProcessor.process();
System.out.println(result);
}
}
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileProcessor {
private String path;
public FileProcessor(String path) {
this.path = path;
}
public int process() {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = 0;
String line;
while ((line = reader.readLine()) != null) {
result += Integer.parseInt(line); // 특정 연산
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
}
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class MultuplyFileProcessor {
private String path;
public MultuplyFileProcessor(String path) {
this.path = path;
}
public int process() {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = 1;
String line;
while ((line = reader.readLine()) != null) {
result *= Integer.parseInt(line); // 특정 연산
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
}
public class Client {
public static void main(String[] args) {
FileProcessor fileProcessor = new MultiplyFileProcessor("number.txt");
int result = fileProcessor.process();
System.out.println(result);
fileProcessor = new PlusFileProcessor("number.txt");
int sumResult = fileProcessor.process();
System.out.println(sumResult);
}
}
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public abstract class FileProcessor {
private String path;
public FileProcessor(String path) {
this.path = path;
}
public final int process() {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = getInitialResult();
String line;
while ((line = reader.readLine()) != null) {
result = calculate(result, Integer.parseInt(line));
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
protected abstract int getInitialResult();
protected abstract int calculate(int result, int number);
}
앞서 말했듯이 추상 클래스가 템플릿을 제공하고, 하위 클래스에서 구체적인 작업을 하면된다!
public class MultiplyFileProcessor extends FileProcessor {
public MultiplyFileProcessor(String path) {
super(path);
}
@Override
protected int getInitialResult() {
return 1;
}
@Override
protected int calculate(int result, int number) {
return result * number;
}
}
public class PlusFileProcessor extends FileProcessor {
public PlusFileProcessor(String path) {
super(path);
}
@Override
protected int getInitialResult() {
return 0;
}
@Override
protected int calculate(int result, int number) {
return result + number;
}
}
다이어그램

템플릿 메소드 패턴의 장점과 단점

템플릿 콜백 패턴 적용 코드
public interface Operator {
abstract int getResult(int result, int number);
}
public class FileProcessor {
private String path;
public FileProcessor(String path) {
this.path = path;
}
public final int process(Operator operator) {
try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = 0;
String line = null;
while((line = reader.readLine()) != null) {
result = operator.getResult(result, Integer.parseInt(line));
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
}
public class Client {
public static void main(String[] args) {
FileProcessor fileProcessor = new FileProcessor("number.txt");
int result = fileProcessor.process((sum, number) -> sum += number);
System.out.println(result);
}
}
공통 로직 제공 (템플릿):
FileProcessor 클래스의 process 메서드는 파일을 읽고, 각 숫자에 대해 동일한 프로세스를 처리하는 공통 구조를 제공한다.특정 로직 위임 (콜백):
Operator 인터페이스가 구현하는 getResult 메서드를 통해, 연산의 세부적인 구현은 호출 시 전달된 람다나 구현체에 위임된다.람다를 활용한 간결한 구현:
Client에서 process 메서드 호출 시 (sum, number) -> sum += number라는 람다를 전달해 합계를 구하는 연산을 정의한다. 이로써 복잡한 상속 구조 없이 원하는 연산을 간단히 구현할 수 있다.템플릿 콜백 패턴의 장단점
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyHelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("Hello, GET!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("Hello, POST!");
}
}
HttpServlet은 템플릿 메소드 패턴을 기반으로, HTTP 요청 메소드(doGet, doPost)의 공통적인 흐름을 제공하고, 세부 구현은 서브클래스에서 정의한다.스프링은 템플릿 메소드 패턴과 템플릿 콜백 패턴을 활용하여 다양한 API를 제공한다.
import org.springframework.jdbc.core.JdbcTemplate;
public class TemplateInSpring {
public static void main(String[] args) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.execute("INSERT INTO users (name) VALUES ('John')");
}
}
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
public class RestTemplateExample {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
headers.set("X-COM-PERSIST", "NO");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:8080/users",
HttpMethod.GET,
entity,
String.class
);
System.out.println(response.getBody());
}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll();
}
}
비지터 패턴은 객체 구조에 대해 작동하는 연산을 객체의 클래스를 수정하지 않고 추가할 수 있는 방법을 제공한다. 이는 더블 디스패치를 활용하여, 실행 시점에 실제 객체와 그에 대한 동작을 결정한다.

public class Client {
public static void main(String[] args) {
Shape rectangle = new Rectangle();
Device device = new Phone();
rectangle.printTo(device);
}
}
public interface Shape {
void printTo(Device device);
}
public class Rectangle implements Shape {
@Override
public void printTo(Device device) {
if (device instanceof Phone) {
System.out.println("Print Rectangle to Phone");
} else if (device instanceof Watch) {
System.out.println("Print Rectangle to Watch");
}
}
}
public interface Device {
}
public class Phone implements Device {
}
public class Watch implements Device{
}
Before 문제점
확장성 문제:
Device 타입이 추가될 때마다 모든 Shape 클래스의 printTo 메서드를 수정해야 한다.의존성 문제:
Rectangle 클래스가 Device의 구체적인 구현(Phone, Watch)을 직접 참조하여 결합도가 높아진다.중복 코드 증가:
Shape에서 유사한 조건문을 반복적으로 작성해야 한다.After 개선 사항
Device를 추가할 때, 기존 Shape 클래스는 변경하지 않고, Device 인터페이스의 메서드를 구현하기만 하면 됨.Shape 클래스는 더 이상 구체적인 Device 구현을 참조하지 않고, Device 인터페이스만 참조함으로써 결합도가 낮아짐.if-else 로직을 제거하고, Shape가 Device의 메서드 호출을 통해 직접 자신의 타입을 전달하도록 개선됨.Shape의 accept 메서드가 호출되면, Device의 print 메서드가 실행되어 객체와 동작의 조합을 런타임에 결정함.public class Client {
public static void main(String[] args) {
Shape rectangle = new Rectangle();
Device device = new Pad();
rectangle.accept(device);
}
}
public interface Shape {
void accept(Device device);
}
Shape의 역할 (Element):
public class Rectangle implements Shape {
@Override
public void accept(Device device) {
device.print(this);
}
}
public interface Device {
void print(Circle circle);
void print(Rectangle rectangle);
void print(Triangle triangle);
}
Device의 역할 (Visitor):
각 Shape를 처리하는 구체적인 로직을 담당.
예: Pad 클래스는 Rectangle, Circle, Triangle 각각에 대해 처리 방식을 정의.
public class Pad implements Device {
@Override
public void print(Circle circle) {
System.out.println("Print Circle to Pad");
}
@Override
public void print(Rectangle rectangle) {
System.out.println("Print Rectangle to Pad");
}
@Override
public void print(Triangle triangle) {
System.out.println("Print Triangle to Pad");
}
}
public class Circle implements Shape {
@Override
public void accept(Device device) {
device.print(this);
}
}
public class Triangle implements Shape {
@Override
public void accept(Device device) {
device.print(this);
}
}
비지터 패턴으로 인한 변화
Shape가 Device를 알고, 구체적인 동작을 스스로 정의.Shape는 자신의 존재를 알리는 역할만.Device가 Shape를 처리하는 모든 동작을 정의.Device를 추가할 때도 기존 Shape는 수정하지 않음.accept(Device device)를 통해 디바이스를 호출하고, 처리 권한을 위임한다.print(Shape shape) 메서드를 통해 구체적인 출력 형식을 정의한다.다이어그램

확장성 향상:
기능 집중화:
Shape 클래스가 Device 인터페이스의 모든 메서드를 처리해야 한다.Shape 클래스가 추가되면, 기존 Device 구현체를 모두 수정해야 한다.1. 자바 API에서 활용
FileVisitor와 SimpleFileVisitor는 자바 NIO 파일 시스템에서 파일 트리 탐색을 처리하기 위해 비지터 패턴을 사용한다.
FileVisitor 예제: 특정 파일 검색import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
public class SearchFileVisitor implements FileVisitor<Path> {
private String fileToSearch;
private Path startingDirectory;
public SearchFileVisitor(String fileToSearch, Path startingDirectory) {
this.fileToSearch = fileToSearch;
this.startingDirectory = startingDirectory;
}
// 디렉토리 방문 전 처리
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Visiting directory: " + dir);
return FileVisitResult.CONTINUE; // 다음으로 계속
}
// 파일 방문 시 처리
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (fileToSearch.equals(file.getFileName().toString())) {
System.out.println("Found file: " + file);
return FileVisitResult.TERMINATE; // 탐색 종료
}
return FileVisitResult.CONTINUE;
}
// 파일 방문 실패 시 처리
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
exc.printStackTrace();
return FileVisitResult.CONTINUE;
}
// 디렉토리 방문 후 처리
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
System.out.println("Finished visiting directory: " + dir);
return FileVisitResult.CONTINUE;
}
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class VisitorInJava {
public static void main(String[] args) throws IOException {
Path startingDirectory = Path.of("/path/to/start"); // 탐색 시작 경로
SearchFileVisitor searchFileVisitor = new SearchFileVisitor("targetFile.txt", startingDirectory);
Files.walkFileTree(startingDirectory, searchFileVisitor);
}
}
FileVisitor의 역할:
preVisitDirectory), 파일 방문 시(visitFile), 방문 실패 시(visitFileFailed), 디렉토리 방문 후(postVisitDirectory)의 단계로 나뉜다.확장 가능성:
walkFileTree)를 수정하지 않고, 새로운 FileVisitor 구현체를 작성하면 된다.실무에서 활용:
2. 스프링 프레임워크에서 비지터 패턴 활용
BeanDefinitionVisitor는 스프링의 빈 정의를 탐색하고, 필요한 변경 작업을 수행할 수 있도록 설계된 클래스이다.
BeanDefinitionVisitor의 역할import org.springframework.beans.factory.config.BeanDefinitionVisitor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
public class CustomBeanVisitor {
public void visitBeans(ConfigurableListableBeanFactory beanFactory) {
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(value -> {
// 특정 값을 마스킹
if (value instanceof String && ((String) value).contains("password")) {
return "******"; // 비밀번호 마스킹 처리
}
return value;
});
for (String beanName : beanFactory.getBeanDefinitionNames()) {
visitor.visitBeanDefinition(beanFactory.getBeanDefinition(beanName));
}
}
}
BeanDefinitionVisitor의 역할:
활용 사례:
확장 가능성:
BeanDefinitionVisitor에 전달할 커스텀 로직을 정의하면 된다.