2024-08-05 ~ 08-11 개념 정리

이선우·2024년 8월 6일
0

개발 관련 지식

목록 보기
3/13

VO란??

VO(Value Object)란 도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을 나타내는 객체를 의미한다.
int, boolean과 같은 속성들은 primitive 속성이다.

VO를 사용하는 이유는 primitive 타입이 도메인 객체를 모델링하기 위해 충분하지 않기 때문이다

도메인 객체란 특정 업무 영역 내에서 중요한 의미를 가지는 데이터와 행동을 나타내는 객체를 말한다. 예를 들어 은행 시스템에서의 도메인 객체는 계좌(Account) 객체, 고객(Customer) 객체가 있다. 이러한 도메인 객체들은 비즈니스 로직을 캡슐화하며, 해당 도메인에 필요한 작업과 규칙을 처리하는 역할을 한다

추가로 모델링이란 간단하게 말하면 복잡한 시스템, 개념, 프로세스 등을 이해하고 설계하기 위해 시각적, 수학적, 논리적 또는 개념적 표현을 만드는 과정이다

그럼 primitive 타입이 도메인 객체를 모델링하기 충분하지 않은 이유는
1. primitive 타입의 기능들을 객체가 전부 사용하지 않는다
예를 들어 사다리 타기 게임에서 너비와 높이를 int 타입으로 설계하면, 너비와 높이는 더해지거나 곱하거나 나누거나 음수가 되는 기능이 필요하지 않지만 int는 방금 말한 기능들을 모두 사용할 수 있다.
2. 한 곳이 아니라 여러 곳에서 사용될 때 중복 코드가 발생한다
너비와 높이는 사다리뿐만이 아니라 직사각형이라는 객체에서도 가질 수 있다
이떄, 사다리와 직사각형은 '너비'와 '높이'의 유효성 검사를 진행한 후에 생성되야 한다
그렇기 때문에 '너비'와 '높이'를 가지는 모든 객체에 중복되는 코드가 들어갈 것이다

public class Ladder {

    private final int width;
    private final int height;

    public void Ladder(int width, int height) {
    	validateWidth(width); // 중복 코드
        validateHeight(height); // 중복 코드
        this.width = width;
        this.height = height;
    }
    
    private void validateWidth() {
    	...
    }
    
    private void validateHeight() {
    	...
    }
}
public class Rectangle {

    private final int width;
    private final int height;

    public void Ladder(int width, int height) {
    	validateWidth(width); // 중복 코드
        validateHeight(height); // 중복 코드
        this.width = width;
        this.height = height;
    }
    
    private void validateWidth() {
    	...
    }
    
    private void validateHeight() {
    	...
    }
}
  1. 한 곳이 아니라 여러 곳에서 사용될 때 불변을 한 번에 보장할 수 없다
    '너비'와 '높이'를 가지는 것이 사다리뿐만이 아니라 직사각형이라는 객체도 가질 수 있는데, 각각의 객체에서 '너비'와 '높이'가 불변인 것을 호출하는 것을 확인하기 위해서는 일일이 Rectangle, Ladder 클래스로 들어가서 int형 width, height에 final 키워드가 붙었는지 확인해야 한다

VO 생성 예시

public final class Address {
    private final String street;
    private final String city;
    private final String postalCode;

    // Constructor to initialize Address object
    public Address(String street, String city, String postalCode) {
        if (street == null || city == null || postalCode == null) {
            throw new IllegalArgumentException("Arguments cannot be null");
        }
        this.street = street;
        this.city = city;
        this.postalCode = postalCode;
    }

    // Getters for the fields
    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }

    public String getPostalCode() {
        return postalCode;
    }

    // Override equals() to ensure that two Address objects are equal if their values are the same
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Address address = (Address) o;

        if (!street.equals(address.street)) return false;
        if (!city.equals(address.city)) return false;
        return postalCode.equals(address.postalCode);
    }

    // Override hashCode() to maintain consistency with equals()
    @Override
    public int hashCode() {
        int result = street.hashCode();
        result = 31 * result + city.hashCode();
        result = 31 * result + postalCode.hashCode();
        return result;
    }

    // Override toString() for better representation
    @Override
    public String toString() {
        return "Address{" +
               "street='" + street + '\'' +
               ", city='" + city + '\'' +
               ", postalCode='" + postalCode + '\'' +
               '}';
    }
}
public class Main {
    public static void main(String[] args) {
        // Create two Address objects with the same values
        Address address1 = new Address("123 Main St", "Springfield", "12345");
        Address address2 = new Address("123 Main St", "Springfield", "12345");

        // Check if they are equal
        System.out.println(address1.equals(address2)); // true

        // Print the address
        System.out.println(address1); // Address{street='123 Main St', city='Springfield', postalCode='12345'}
    }
}

Java Exception에 대하여

프로그래밍을 할 때 수많은 에러와 예외를 마주할 때 프로그램은 의도한 대로 작동하지 않거나 실행을 비정상적으로 종료한다
이러한 에러와 예외들이 발생할 가능성이 있을 때, 효과적으로 처리하는 방법이 예외 처리이다

예외 처리란? (Exception Handling)

예외 처리는 코드 작성자가 예기치 않게 발생하는 에러들에 대응할 수 있도록 사전에 방지하는 것으로, 예외 처리를 하면 프로그램의 비정상적인 종료를 방지하여 정상적인 실행 상태를 유지할 수 있다

throws

예외가 발생하면 던질 수 있다,,! 그 말은 예외를 여기서 처리하지 않을테니 나를 불러다가 쓰는 녀석에게 에러 처리를 전가하겠다는 의미이다.

public static void divide(int a,int b) throws ArithmeticException {
	if(b==0) throw new ArithmeticException("0으로 나눌 수는 없다니까?");
	int c=a/b;
	System.out.println(c);
}
public static void main(String[] ar){
	int a=10;
	int b=0;
		
	divide(a,b);
}

커스텀 예외(Custom Exception)를 언제 써야 할까??

가게에서 손님한테 과자주문을 받는다고 할 때 가지고 있는 수량보다 주문 수량이 많으면 주문을 못받는 예외처리를 만들 때 커스텀 예외를 구현해서 사용한다

  1. Product 클래스에는 order 메소드가 구현되어 있다. try catch를 통해서 잔고보다 많은 주문량이 발생되면 특정 물품의 잔고가 부족하다는 에러메시지가 발생되게 하고 싶다
public void order(int jumunsu) throws JangolackException {
		
		if(jango < jumunsu) {
			
            //***특정에러메시지를 전달한다***
            
			throw new JangolackException(">> " + prodName +"은 잔고가 "
            + jango +"개 인데 주문량이 "+ jumunsu 
            +"개라서 잔고 부족으로 주문이 불가합니다.<<"); 
		} 

		else {
			jango -= jumunsu;
			System.out.println(prodName + "제품을" + jumunsu + "개 주문하셨습니다");
		}
	}
    ```
    
```java
public class JangolackException extends Exception {
	
	static final long serialVersionUID = 1L;
	
    // 1. 기본생성자를 활용한 예외처리
	public JangolackException() {         
		super(">> 잔고량이 주문량보다 적으므로 주문이 불가합니다. <<");
	}
	
	
	// 2. String 에러메시지 파라미터를 받는 예외처리 방법
	public JangolackException(String errMsg) {
		super(errMsg);                        // Super를 통해 부모클래스인 Exception을 활용
	}
}

일반 예외로 선언할 경우 Exception을 상속받아 구현
실행 예외로 선언할 경우 RuntimeException을 상속받아 구현

일반 예외란? 컴파일러가 체크하는 예외
실행 예외란? 컴파일러가 체크하지 않는 예외

사용자 정의 예외 클래스를 작성 시에는 생성자를 두 개 선언하는 것이 일반적이다

  • 매개 변수가 없는 기본 생성자
  • 예외 발생 원인(예외 메시지)을 전달하기 위해 String 타입의 매개변수를 갖는 생성자

ex)

public class CustomException extends RuntimeException {

    // 1. 매개 변수가 없는 기본 생성자
    CustomException() {

    }

    // 2. 예외 발생 원인(예외 메시지)을 전달하기 위해 String 타입의 매개변수를 갖는 생성자
    CustomException(String message) {
        super(message); // RuntimeException 클래스의 생성자를 호출합니다.
    }
}

printStackTrace() 란?

  • 예외 발생 코드를 추적해서 모두 콘솔에 출력한다
  • 어떤 예외가 어디에서 발생했는지 상세하게 출력해주기 때문에 프로그램을 테스트하면서 오류를 찾을 때 활용한다

정규식이란?

정규표현식(Regular Expression)이란
특정 문자열의 규칙을 가지는 문자열의 집합을 표현하는 데 사용되는 언어

Pattern Class

  • 정규식을 기반으로 문자열에 대한 검증을 수행한다

Pattern.matcher(String regex, CharSequence input)
// 대상이 되는 문자열과 정규식이 일치하는 경우 true, 아닌 경우 false를 반환

Pattern.pattern()
// 컴파일된 정규표현식을 String 형태로 반환

Pattern.compile(String regex)
// 컴파일된 정규 표현식을 반환

Pattern.asPredicate()
// 문자열을 일치시키는 데 사용할 수 있는 술어를 작성

Pattern.split(CharSequence input)
// 문자열을 주어진 인자값 CharSequence 패턴에 따라 분리

Matcher Class

  • 문자열 내에 일치하는 문자열을 확인하기 위해 정규식을 이용하여 찾고 존재여부를 반환해 주는 함수

matches() // 대상 문자열과 패턴이 일치할 경우 true를 반환

find() // 대상 문자열과 패턴이 일치하는 경우 true를 반환하고, 그 위치로 이동

find(int start) // start위치 이후부터 매칭검색을 수행

start() // 매칭되는 문자열 시작위치 반환

start(int group) // 지정된 그룹이 매칭되는 시작위치를 반환

end() // 매칭되는 문자열 끝 다음 문자위치 반환

end(int group) // 지정된 그룹이 매칭되는 끝 다음 문자위치 반환

group() // 매칭된 부분을 반환

group(int group) // 매칭된 부분중 group번 그룹핑 매칭부분 반환

groupCount() // 패턴내 그룹핑한(괄호지정) 전체 갯수를 반환

트랜잭션이란?

트랜잭션이란 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위를 뜻한다
SELECT, INSERT, DELETE, UPDATE와 같은 SQL을 이용해 데이터베이스에 접근 하는 것을 상태를 변화시킨다고 한다

작업 단위는 많은 질의어 명령문들을 사람이 정하는 기준에 따라 정하는 것을 의미한다

예를 들어 게시판 사용자는 게시글을 작성하고, 올리기 버튼을 누른다. 그 후에 다시 게시판에 돌아왔을 때, 게시판은 자신의 글이 포함된 업데이트된 게시판을 보게된다.
이러한 상황에서는 Insert 와 Select를 사용해야 하는데 여기서 작업의 단위는 insert문과 select문 둘 다 합친 것이라 말하고 이러한 작업단위를 하나의 트랜잭션이라고 한다

트랜잭션의 특징

  1. 원자성
    트랜잭션이 데이터베이스에 모두 반영되던가, 아니면 전혀 반영되지 않아야 한다
  2. 일관성
    트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다
  3. 독립성
    둘 이상의 트랜잭션이 동시에 실행되고 있을 경우 어떤 하나의 트랜잭션이라도, 다른 트랜잭션의 연산에 끼어들 수 없다
  4. 지속성
    트랜잭션이 성공적으로 완료되었을 경우, 결과는 영구적으로 반영되어야 한다

Service 에서의 @Transactional

스프링에서는 @Transactional을 활용한 선언적 트랜잭션 관리와, 직접 트랜잭션 매니저를 이용해 트랜잭션 코드를 작성하는 프로그래밍 방식 트랜잭션 관리가 있다

프로그래밍 방식 트랜잭션 관리의 단점

  • 프로그래머가 매번 트랜잭션과 관련된 로직을 작성
  • 비즈니스 로직과 트랜잭션이 관련된 코드가 분리 X

@Transactional가 Proxy 객체를 활용해 해결

  • @Transactional는 코드의 반복, 순수한 비즈니스 로직을 Proxy를 통해 가능하게 한다
  • 내부적으로 @Transactional이 붙은 클래스의 Proxy를 스프링 AOP를 통해 구현한다
  • 서비스 계층을 호출하는 입장에서는 Proxy인 것을 몰라도 되기 때문에 코드 수정이 필요없다

스프링 AOP와 Proxy

스프링 프레임워크에서 비즈니스 로직과 공통 관심사를 분리하기 위해 사용하는 도구
스프링 AOP
관심사를 모듈화하여 코드의 분리도를 높이고, 공통적으로 사용되는 기능을 여러 곳에서 일관되게 적용할 수 있도록 한다.

스프링 Proxy
스프링에서 AOP를 구현하는 주요 방법 중 하나로 다른 객체에 대한 접근을 제어하는 객체
Proxy의 유형
1. JDK Dynamic Proxy
인터페이스 기반 프록시로 대상 객체가 구현하는 인터페이스를 기반으로 프록시를 생성
2. CGLIB Proxy
클래스 기반 프록시로 대상 클래스의 서브클래스를 생성하여 프록시 객체를 생성


@Converter이란?

Converter는 JPA에서 데이터베이스와 자바 객체 간의 데이터 변환을 처리하는 데 사용된다
JPA의 AttributeConverter 인터페이스를 구현하여 데이터베이스 필드와 자바 객체 사이의 변환 로직을 정의할 수 잇다.
예를들어 java에서는 'boolean'으로 값을 저장하고, DB에는 String 타입의 'Y' 또는 'N'으로 저장하고 싶을 때 사용한다

ex) @Converter를 사용하여 Enum 변환하기
가장 일반적인 예제가 데이터베이스에 Enum 값을 저장하고 자바 객체에서는 Enum 타입으로 다루는 것이다.

  1. Enum 정의
public enum Status {
    ACTIVE,
    INACTIVE,
    PENDING
}
  1. Converter 구현
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class StatusConverter implements AttributeConverter<Status, String> {

    @Override
    public String convertToDatabaseColumn(Status attribute) {
        if (attribute == null) {
            return null;
        }
        return attribute.name();
    }

    @Override
    public Status convertToEntityAttribute(String dbData) {
        if (dbData == null) {
            return null;
        }
        return Status.valueOf(dbData);
    }
}
  1. 엔티티에서 사용
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Convert;

@Entity
public class User {

    @Id
    private Long id;

    @Convert(converter = StatusConverter.class)
    private Status status;

    // getters and setters
}

이 예제에서 StatusConverterStatus Enum을 데이터베이스에 저장할 때 String으로 변환하고 데이터베이스에서 읽을 때 다시 Status Enum으로 변환한다.

이렇게 하면 JPA와 데이터베이스 간의 데이터 변환을 쉽게 관리할 수 있다


GlobalExceptionHandler

GlobalExceptionHandler는 스프링 프레임워크에서 애플리케이션 전역에서 발생하는 예외를 처리하기 위한 구성 요소이다.

애플리케이션의 모든 예외를 잡아서 처리할 수 있는 전역 예외 처리기를 구현하는 방법으로 이를 통해 애플리케이션의 모든 레이어에서 발생할 수 있는 예외를 중앙 집중식으로 처리하고, 일관된 에러 응답을 사용자에게 제공할 수 있다.

구현 방법

  1. @ControllerAdvice 어노테이션
    @ControllerAdvice는 특정 컨트롤러가 아닌 전체 애플리케이션에 대해 예외를 처리할 수 있는 클래스에 붙이는 어노테이션이다. 이 어노테이션을 사용하면 전역적으로 예외를 처리할 수 있다

  2. @ExceptionHandler 어노테이션
    @ExceptionHandler는 특정 예외를 처리하는 메소드를 정의할 때 사용된다.

  3. @ResponseStatus 어노테이션
    @ResponseStatus를 사용하여 HTTP 상태 코드를 설정할 수 있다

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class GlobalExceptionHandler {

    // 특정 예외를 처리하는 핸들러 메소드
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
    }

    // 모든 예외를 처리하는 핸들러 메소드
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseEntity<String> handleException(Exception ex) {
        // 로그를 남기거나 추가적인 처리 가능
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred.");
    }
}

GlobalExceptionHandler를 사용하면 애플리케이션의 예외를 중앙에서 관리하고, 클라이언트에게 일관된 오류 응답을 제공할 수 있다


BindingResult

BindingResult는 스프링 프레임워크에서 폼 데이터를 바인딩하고 유효성 검사를 수행할 때 사용되는 객체이다. 주로 웹 애플리케이션에서 사용자 입력을 검증하고 처리할 때 사용된다.

폼 데이터 바인딩

  • 사용자가 웹 폼에 입력한 데이터는 스프링의 @ModelAttribute 어노테이션을 통해 자바 객체에 바인딩된다. 이 과정에서 BindingResult를 사용해서 바인딩 및 검증 결과를 확인할 수 있다

유효성 검사

  • @Valid 또는 @Validated 어노테이션을 사용해서 입력 데이터의 유효성 검사를 수행하고 검사 결과는 BindingResult에 저장된다

오류 처리

  • 유효성 검사가 실패하면 BindingResult 객체를 통해 오류 메시지를 처리하고, 이를 클라이언트에게 전달하거 폼에 다시 표시할 수 있다
  1. 폼 객체 정의
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class UserForm {

    @NotEmpty(message = "Username cannot be empty")
    private String username;

    @Size(min = 8, message = "Password must be at least 8 characters long")
    private String password;

    // getters and setters
}
  1. 컨트롤러 메소드 정의
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ModelAttribute;

import javax.validation.Valid;

@Controller
public class UserController {

    @GetMapping("/register")
    public String showRegistrationForm(Model model) {
        model.addAttribute("userForm", new UserForm());
        return "registration";
    }

    @PostMapping("/register")
    public String registerUser(@ModelAttribute("userForm") @Valid UserForm userForm, BindingResult result) {
        if (result.hasErrors()) {
            // 유효성 검사 오류가 있는 경우 폼을 다시 표시
            return "registration";
        }
        // 성공적으로 등록된 경우 처리 로직
        return "success";
    }
}
  1. 뷰 템플릿
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Registration Form</title>
</head>
<body>
    <h2>Registration Form</h2>
    <form th:action="@{/register}" th:object="${userForm}" method="post">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" th:field="*{username}" />
            <span th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></span>
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" th:field="*{password}" />
            <span th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></span>
        </div>
        <button type="submit">Register</button>
    </form>
</body>
</html>

profile
백엔드 개발자 준비생

0개의 댓글