AddOperation
public class AddOperation implements Operation{
@Override
public double operate(int num1, int num2) {
return num1 + num2;
}
}
BadInputException
public class BadInputException extends Exception {
public BadInputException(String type) {
super("잘못된 내용입니다!"+type+" 을 입력해주세요.");
}
}
Calculator
public class Calculator {
private Operation operation;
private int firstNumber;
private int secondNumber;
public Calculator() {
}
/*
feat: week4
*/
public Calculator(Operation operation) {
this.operation = operation;
}
public void setOperation(Operation operation) {
this.operation = operation;
}
// public double calculate(int num1, int num2) {
// if (operation == null) throw new IllegalStateException("연산자 오류");
// return operation.operate(num1, num2);
// }
/*
feat: week4
*/
public void setFirstNumber(int firstNumber) {
this.firstNumber=firstNumber;
}
public void setSecondNumber(int secondNumber) {
this.secondNumber = secondNumber;
}
public double calculate(){
// double answer = 0;
// answer = operation.operate(this.firstNumber, this.secondNumber);
// return answer;
if (operation == null) {
throw new IllegalStateException("연산자가 설정되지 않았습니다");
}
return operation.operate(this.firstNumber, this.secondNumber);
}
}
CalculatorApp
import java.io.*;
import java.util.*;
public class CalculatorApp {
public static boolean start() throws Exception {
Parser parser = new Parser();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
System.out.println("첫번째 숫자를 입력해주세요 !");
String firstInput = reader.readLine();
parser.parseFirstNum(firstInput);
System.out.println("연산자를 입력해주세요 !");
String operator = reader.readLine();
parser.parseOperator(operator);
System.out.println("두번째 숫자를 입력해주세요 !");
String secondInput = reader.readLine();
parser.parseSecondNum(secondInput);
System.out.println("연산 결과 : " + parser.executeCalculator());
return true;
} catch (IOException e) {
System.err.println("오류 발생: " + e.getMessage());
return false;
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
System.err.println("리소스 해제 중 오류가 발생: " + e.getMessage());
}
}
}
}
DivideOperation
public class DivideOperation implements Operation{
public double operate(int num1, int num2) {
if(num2 == 0) throw new ArithmeticException("나눌 수 없습니다.");
return (double) num1 / num2;
}
}
Main
public class Main {
public static void main(String[] args) {
Calculator ccl = new Calculator();
boolean calculateEnded = false;
//구현 2.
while (!calculateEnded) {
try {
calculateEnded = CalculatorApp.start();
} catch (Exception e) {
System.out.println("에기치 못한 오류 발생:" + e.getMessage());
}
}
}
}
MultiplyOperation
public class MultiplyOperation implements Operation{
public double operate(int num1, int num2) {
return num1 * num2;
}
}
Operation
// 추상화를 위해 추가함
public interface Operation {
double operate(int num1, int num2);
}
SubstractOperation
public class SubstractOperation implements Operation{
public double operate(int num1, int num2) {
return num1 - num2;
}
}
Parser
import java.util.regex.Pattern;
public class Parser {
private static final String OPERATION_REG = "[+\\-*/]";
private static final String NUMBER_REG = "^[0-9]*$";
private final Calculator calculator = new Calculator();
public Parser parseFirstNum(String firstInput) throws BadInputException {
// 구현 1.
if (!Pattern.matches(NUMBER_REG, firstInput)) throw new BadInputException("숫자");
calculator.setFirstNumber(Integer.parseInt(firstInput));
return this;
}
public Parser parseSecondNum(String secondInput) throws BadInputException {
// 구현 1.
if (!Pattern.matches(NUMBER_REG, secondInput)) {
throw new BadInputException("숫자");
}
calculator.setSecondNumber(Integer.parseInt(secondInput));
return this;
}
public Parser parseOperator(String operationInput) throws BadInputException {
// 구현 1.
if (!Pattern.matches(OPERATION_REG, operationInput)) {
throw new BadInputException("연산자");
}
switch (operationInput) {
case "+":
calculator.setOperation(new AddOperation());
break;
case "-":
calculator.setOperation(new SubstractOperation());
break;
case "*":
calculator.setOperation(new MultiplyOperation());
break;
case "/":
calculator.setOperation(new DivideOperation());
break;
}
return this;
}
public double executeCalculator() {
return calculator.calculate();
}
}
return this
란 무엇일까?여러 메서드를 연속으로 호출할 수 있도록 하는 패턴입니다.
각 메서드가 객체 자신(this)를 반환하기 때문에, 메서드를 연속적으로 호출할 수 있는 것입니다.
public class Example {
private int value;
public Example setValue(int value) {
this.value = value;
return this; // 현재 객체 자신을 반환
}
public Example printValue() {
System.out.println("Value: " + this.value);
return this; // 현재 객체 자신을 반환
}
public static void main(String[] args) {
Example example = new Example();
example.setValue(5).printValue();
}
}
/*
* 출력
* value: 5
*/
그렇다면 Perser 클래스에서의 return this를 사용하면 얻는 이점은 ?
메서드 체이닝
Parser parser = new Parser();
parser.parseFirstNum("5")
.parseOperator("+")
.parseSecondNum("3");
유연하고 간결한 코드
IF 메서드 체이닝이 없다면 ?
Parser parser = new Parser();
parser.parseFirstNum("5");
parser.parseOperator("+");
parser.parseSecondNum("3");
여기서 나는 궁금해졌다.
꼭 return this를 사용해서만 메서드 체이닝이 가능한 것일까 ?
이전에 프로젝트 할 때, 시큐리티 부분에서 필터 체인 한 것이 기억이 났다.
시큐리티에서 HttpSecurity 설정은 대표적인 메서드 체이닝
http
.authorizeRequests()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
AuthorizeRequestConfigurer configurer = http.authorizeRequests();
configurer.antMatchers("/public").permitAll();
이런 방식으로 메서드 체이닝은 return this 없이도 가능하지만, 코드의 가독성은 낮아지고, 일관성이 떨어질 수 있기 때문에 지양하는 것이 좋습니다.
이를 개선한 것이 HttpSecurity 내에서 적극적으로 사용되는 Builder 패턴입니다.
return this 대신 Builder 객체를 반환하면서 체인을 이어가는 것입니다.
http
.authorizeRequests(new AuthorizeRequestConfigurer())
.formLogin(new FormLoginConfigurer());
이런 식으로, 각 단게가 새로운 설정 객체를 반환해 메서드 체이닝처럼 보이게 됩니다.
좀 더 자세한 HttpSecurity 예시 코드
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 요청 권한 설정 (AuthorizeRequestsConfigurer 반환)
/*
역할: 요청 URL 패턴별로 접근 권한을 설정합니다.
빌더 패턴 활용: authorizeRequests()가 AuthorizeRequestsConfigurer를 반환하여
.antMatchers()와 같은 추가 설정이 가능
*/
.authorizeRequests(auth -> auth
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 2. 로그인 설정 (FormLoginConfigurer 반환)
/*
역할: 로그인 페이지 및 성공/실패 URL을 설정합니다.
빌더 패턴 활용: formLogin()가 FormLoginConfigurer를 반환하여
.loginPage()와 같은 메서드 체이닝이 가능합니다.
*/
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
.failureUrl("/login?error=true")
.permitAll()
)
// 3. 로그아웃 설정 (LogoutConfigurer 반환)
/*
역할: 로그아웃 동작과 로그아웃 후 이동할 URL을 설정합니다.
빌더 패턴 활용: logout()가 LogoutConfigurer를 반환합니다.
*/
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
// 4. CSRF 비활성화 (CsrfConfigurer 반환)
/*
역할: CSRF 보호 설정을 관리합니다.
빌더 패턴 활용: csrf()가 CsrfConfigurer를 반환하여
.disable()과 같은 메서드 체이닝이 가능합니다.
*/
.csrf(csrf -> csrf.disable())
// 5. 예외 처리 (ExceptionHandlingConfigurer 반환)
/*
역할: 접근 거부 페이지 및 예외 처리 전략을 설정합니다.
빌더 패턴 활용: exceptionHandling()이 ExceptionHandlingConfigurer를 반환
*/
.exceptionHandling(exception -> exception
.accessDeniedPage("/403")
);
// 6. 최종 빌드
/*
http.build()는 SecurityFilterChain 객체를 반환합니다.
이 객체는 Spring Security가 HTTP 요청을 보호하기 위해 사용하는 핵심 보안 체인이다.
*/
return http.build();
}
}
Spring Security의 HttpSecurity는 빌더 패턴을 사용하여 각 보안 기능을 모듈화하고, 설정 단계를 명확하게 구분합니다.
메서드 체이닝을 통해 HttpSecurity는 연속적인 보안 설정을 가능하게 합니다.
각 단계는 새로운 설정 객체(예: AuthorizeRequestsConfigurer, FormLoginConfigurer)를 반환합니다.
authorizeRequests()
→ AuthorizeRequestsConfigurer
반환
formLogin()
→ FormLoginConfigurer
반환
csrf()
→ CsrfConfigurer
반환
각 설정 객체는 특정 보안 영역만을 담당합니다.
예를 들어, FormLoginConfigurer는 로그인 관련 설정만 처리합니다.
즉,