Java 기술과제

ChoRong0824·2025년 1월 8일
0

Java

목록 보기
36/41
post-thumbnail

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 란 무엇일까?
    궁금하시지 않으십니까?
    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
*/
  1. setValue(5)는 this(현재 객체)를 반환합니다.
  2. 이어서 printValue() 메서드가 동일한 객체에서 호출됩니다.
  3. 이런 방식으로 여러 메서드를 한 줄로 이어서 사용할 수 있는 것입니다.

그렇다면 Perser 클래스에서의 return this를 사용하면 얻는 이점은 ?

  1. 메서드 체이닝

    • parseFirstNum, parseSecondNum, parseOperator 를 연속으로 호출 가능하다.
    Parser parser = new Parser();
     parser.parseFirstNum("5")
      .parseOperator("+")
      .parseSecondNum("3");
    
  2. 유연하고 간결한 코드

    • 각 메서드 호출마다 객체를 다시 받아오지 않아도 된다. (코드가 간결& 직관적이게 된다)

IF 메서드 체이닝이 없다면 ?

Parser parser = new Parser();
parser.parseFirstNum("5");
parser.parseOperator("+");
parser.parseSecondNum("3");
  • 각 메서드가 호출될 때마다 새 객체를 반환하지 않게 된다.
    즉, 코드는 길어지고 덜 직관적이게 되는 것이다. (위에 사용한 코드와 대조해서 보면 좋다)

여기서 나는 궁금해졌다.
꼭 return this를 사용해서만 메서드 체이닝이 가능한 것일까 ?
이전에 프로젝트 할 때, 시큐리티 부분에서 필터 체인 한 것이 기억이 났다.

1. Spring Security에서의 메서드 체이닝

시큐리티에서 HttpSecurity 설정은 대표적인 메서드 체이닝

http
   .authorizeRequests()
       .antMatchers("/public").permitAll()
       .anyRequest().authenticated()
   .and()
   .formLogin()
       .loginPage("/login")
       .permitAll();
  • 각 메서드 호출이 다른 설정 객체나 같은 객체(HttpSecurity)를 반환한다.
  • 그리고 제가 알기론, 대부분 HttpSecurity는 return this 패턴을 사용합니다.

2. return this 없이 메서드 체이닝 구현

1) 가능하지만 매우 제한적이다.

  • return this 없이 메서드 체이닝을 구현하려면 중간 객체를 반환하는 구조가 꼭 필요합니다.
    - 예) authorizeRequests()는 AuthorizeRequestConfigurer라는 다른 객체를 반환합니다.
  • Spring Security도 이런 방식으로 각 메서드가 다른 구성 객체로 연결됩니다.
    예)
    	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는 빌더 패턴을 사용하여 각 보안 기능을 모듈화하고, 설정 단계를 명확하게 구분합니다.

핵심

1️⃣ 메서드 체이닝

메서드 체이닝을 통해 HttpSecurity는 연속적인 보안 설정을 가능하게 합니다.
각 단계는 새로운 설정 객체(예: AuthorizeRequestsConfigurer, FormLoginConfigurer)를 반환합니다.

2️⃣ 다른 설정 객체 반환

authorizeRequests()AuthorizeRequestsConfigurer 반환
formLogin()FormLoginConfigurer 반환
csrf()CsrfConfigurer 반환

3️⃣ 설정 객체의 역할 분리

각 설정 객체는 특정 보안 영역만을 담당합니다.
예를 들어, FormLoginConfigurer는 로그인 관련 설정만 처리합니다.

4️⃣ 빌더 패턴과 메서드 체이닝의 관계

5️⃣ 메서드 체이닝과 빌더 패턴의 차이점


즉,

  • return this 없이도 메서드 체이닝은 가능하지만, 코드 가독성과 일관성이 떨어질 가능성이 높습니다.
  • Spring Security는 이 문제를 다른 설정 객체 반환 방식과 return this를 혼합하여 해결했습니다.
  • 결국, 설계 목표에 따라 둘 중 어떤 방법을 사용할지는 상황에 따라 달라질 수 있습니다.

profile
백엔드를 지향하며, 컴퓨터공학과를 졸업한 취준생입니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

0개의 댓글