-
[2024_동계_모각코] 4회차(01/24)[CNU] Mogakco 2024. 2. 18. 23:18
4회차 목표
equals(), hashcode()
IoC와 DI
AOP
1. equals(), hashcode()
자바에서 equals()와 hashCode()는
객체 동등성 비교와 해시 기반 컬렉션에 사용되는 두 가지 메서드.
- equals() 메서드
두 개의 객체가 동등하다고 판단하는 로직을 구현하는데 사용된다.자바에서 모든 클래스는 기본적으로 equals()를 오버라이딩하지 않으면 Object 클래스의 equals() 메서드를 상속받는데, 그 메서드는 객체의 레퍼런스 비교를 수행하기 때문에 두 객체가 메모리에서 같은 위치를 가리키는지를 비교함. - 하지만 대부분의 경우에 우리는 객체의 내용을 기반으로 동등성을 판단하고 싶을 때가 많기 때문에 이럴 때는 반드시 오버라이딩 해줘야 한다.
- 즉, 두 객체의 내용이 동일한지를 확인하는 메서드.
오버라이딩 시 주의할 점
- null 체크: equals() 메서드는 항상 null과 비교되는지 확인하여 NullPointerException을 방지해야 함.
- 자기 자신과의 비교: equals() 메서드는 객체가 자기 자신과 비교될 때 true를 반환해야 함.
- 클래스 타입 체크: equals() 메서드는 다른 클래스의 인스턴스와 비교되지 않도록 첫 번째로 타입을 체크해야 함.
- 내용 비교: 객체의 필드들을 비교하여 내용이 동일한지 확인.
public class Person { private String name; private int age; @Override public boolean equals(Object o) { if (this == o) return true; // 자기 자신과 비교 // null 체크, 클래스 타입 체크 if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); //내용 비교 } }
- hashCode() 메서드
hashCode() 메서드는 객체의 해시 코드를 반환하는데 사용.자바에서 hashCode() 메서드를 오버라이딩하지 않으면 Object 클래스의 기본 hashCode()를 상속받는데, 그 메서드는 객체의 메모리 주소에 기반하여 해시 코드를 생성한다.즉, 두 객체가 equals() 메서드를 통해 동등하다면, hashCode() 메서드도 같은 값을 반환해야 한다. 그렇지 않으면 해시 기반 컬렉션에서 객체를 올바르게 저장하고 검색할 수 없다.또한, hashCode()를 오버라이딩할 때에는 equals()와 마찬가지로 null 값을 다루는 등의 주의사항을 지켜야 한다. - 일반적으로 hashCode()를 구현할 때는 객체 내의 필드들을 기반으로 해시 코드를 계산한다.
- hashCode() 메서드를 오버라이딩할 때는 equals()와 일관성을 유지해야 하는게 포인트.
- 해시 코드는 해시 기반 컬렉션(HashMap, HashSet ..)에서 객체를 저장하고 검색하기 위한 인덱스로 사용된다.
public class Person { private String name; private int age; @Override public boolean equals(Object o) { // equals 메서드 구현 생략 } @Override public int hashCode() { return Objects.hash(name, age); // 필드를 기반으로 해시코드 계산 } }
equals() 메서드를 재정의할 때에는,
- NullPointerException을 발생시키지 않도록 null 값을 처리.
- 객체 비교의 일관성(Consistency)을 유지.
- 객체의 내용이 변경되지 않는 한 항상 같은 결과를 반환해야 하도록.
- 타입 검사를 포함.
- null이 아닌 다른 객체와 비교할 때에는 먼저 클래스 타입을 확인.
- 다른 클래스의 인스턴스와 비교하면 false를 반환.
- 상속을 고려.
- 클래스를 상속할 수 있는 경우, equals() 메서드를 재정의할 때는 상위 클래스의 equals() 메서드를 호출할 수도 있음.
- 필드들의 비교.
- 객체의 내용을 비교하는데 사용되는 필드들을 기반으로 equals() 메서드를 구현.
- hashCode()와 일관성을 유지.
- equals() 메서드를 오버라이딩하면 hashCode() 메서드도 함께 재정의해야 함.
- 동등한 객체는 반드시 같은 해시 코드를 반환해야 함.
hashcode() 를 오버라이딩 할 때에는,
- 같은 객체들은 같은 해시 코드를 반환해야 함.
- equals() 메서드를 통해 동등하다고 판단된 두 객체는 반드시 같은 해시 코드를 반환해야 함.
- 즉, a.equals(b)가 true라면, a.hashCode()와 b.hashCode()는 같아야 한다.
- 다른 객체들은 다른 해시 코드를 반환해야 함.
- 서로 다른 객체는 다른 해시 코드를 반환해야 함.
- 같은 필드를 넘기는 코드를 짜지만 않으면 (해시 충돌이 발생하지만 않으면) 같은 해시 코드가 나올리는 없음.
- null 값을 다루는 방법을 결정해야 함.
- 예를 들어, hashcode 에 넘기는 필드 중 하나가 null일 경우 해시 코드를 어떻게 생성할 것인지 고려해야 한다.
- 해시 충돌 고려.
- 해시 함수는 다양한 입력에 대해 가능한 한 서로 다른 해시 코드를 반환해야 하는데, 해시 충돌은 완전히 피할 수 없다고 한다.
- 해시 충돌이 발생하면 해당 인덱스에 있는 객체들을 검색하는 데 성능 저하가 발생할 수 있으므로, 충돌을 최소화하는 방법을 고려해야 한다.
Spring
1. IoC (Inversion of Control) & DI (Dependency Injection)
- 제어의 역전(IoC):
말 그대로 프로그램의 제어 흐름을 반대로 한다는 뜻. (개발자 → 프레임워크로)프레임워크가 대신 처리해준다! 객체지향 프로그래밍에서의 제어 흐름을 개발자가 정하는 것이 아니라, 프레임워크 또는 컨테이너의 제 3자가 제어 흐름을 주도한다는 개념. 즉 한마디로, 개발자가 프로그램의 제어 흐름을 결정하지 않고, 프레임워크에 의해 제어 흐름이 결정되는 것. 이렇게 하면 애플리케이션의 결합도가 낮아지고 유연성과 확장성이 향상된다.
구현하는 방법
- Dependency Lookup주로 서비스 로케이터 패턴을 사용.Service Locator는 애플리케이션에 필요한 모든 서비스에 접근하는 방법을 알고 있다.
이런 식으로 ServiceLocator 를 싱글턴 레지스트리로 만들어서 직접 접근하는 식으로 인스턴스를 만들면 된다.MovieFinder finder = ServiceLocator.movieFinder(); // class ServiceLocator...
- 따라서 다른 모든 서비스들은 해당 컴포넌트만 알면 된다.
- **서비스 로케이터 패턴?)
- 애플리케이션의 객체를 생성하거나 검색하기 위해 컨테이너에 의존한다.
- Dependency Injection(DI)
- 의존성 주입(Dependency Injection, DI):
위에서 말했듯이 DI는 IoC의 한 형태. 객체가 직접 자신이 사용하는 의존성을 생성하지 않고, 외부에서 주입받는 방식. 이를 통해 객체 간의 결합도가 낮아지며 테스트 용이성과 코드 재사용성이 향상된다.
- 의존성 주입(Dependency Injection, DI):
- 객체가 필요로 하는 의존성을 외부에서 주입받도록 한다.
DI를 사용하는 방법의 3가지 유형
- Constructor Injection (생성자 주입): 생성자 매개변수로 주입.
public class UserService { private UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } // Methods using userRepository... }
- Setter Injection (세터 주입): 의존성을 설정하는 메서드(setter)를 통해 주입.
public interface MessageService { void sendMessage(String message); } // MessageService 를 구현하는 두 클래스 public class EmailService implements MessageService { @Override public void sendMessage(String message) { // Logic to send an email System.out.println("Sending an email: " + message); } } public class SMSService implements MessageService { @Override public void sendMessage(String message) { // Logic to send an SMS System.out.println("Sending an SMS: " + message); } } // MessageService 가 필요한 클래스 public class MessageProcessor { private MessageService messageService; // Setter method public void setMessageService(MessageService messageService) { this.messageService = messageService; } public void processMessage(String message) { // Delegate the message sending to the MessageService implementation messageService.sendMessage(message); } }
- Interface Injection (인터페이스 주입): 특정 인터페이스를 구현하여 의존성을 주입받도록.
// NotificationService.java @Service public class NotificationService { private final MessageService messageService; @Autowired public NotificationService(MessageService messageService) { this.messageService = messageService; } public void sendNotification(String message) { messageService.sendMessage(message); } } // MainApplication.java public class MainApplication { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); NotificationService notificationService = context.getBean(NotificationService.class); notificationService.sendNotification("Hello, Spring Interface Injection!"); } }
정리하자면,
IoC와 DI는 객체지향 프로그래밍에서 애플리케이션의 제어 흐름과 의존성을 관리하는 방법을 제시하는 중요한 개념이다.
Bean을 사용하는 이유
- 애플리케이션이 성장하고 복잡해질 수 있기 때문에, 향후 기능 확장이나 변경을 고려해서 코드를 유연하게 설계하는 것.
- ⇒ Bean을 사용하면 인터페이스를 통해 의존성을 주입하고, DI를 통해 구현 클래스를 변경하거나 새로운 클래스를 손쉽게 추가할 수 있다.
- 테스트 하기 좋다
- ⇒ Bean을 사용하면 의존성을 주입하므로 테스트에서 모의 객체(Mock)를 사용하여 단위 테스트를 수행할 수 있다. 따라서 테스트의 격리성과 정확성을 높이는 데 도움이 됨.
- 결합도 낮추기⇒ Bean을 사용하면 인터페이스를 통해 결합도를 낮추고, 클래스 간의 의존성을 줄일 수 있다.
- 구체적인 클래스를 하나만 사용하더라도 이를 직접적으로 참조하는 것은 객체간의 결합도를 높인다.
- Bean 생명 주기 관리(초기화, 소멸 시점 설정)를 통해 리소스 누수를 방지하고, 애플리케이션의 성능을 최적화할 수 있다.
- AOP(Aspect-Oriented Programming) 지원
- ⇒ Spring은 AOP를 통해 핵심 비즈니스 로직과 부가적인 기능(로깅, 보안, 트랜잭션 …)을 분리하여 개발할 수 있도록 지원한다. 이로 인해 코드의 가독성과 유지 보수성이 향상된다.
따라서)
확정성, 유지 보수, 테스트 용이성, 여러가지 편한 기능 제공 등의 이유 때문에 스프링은 Bean을 사용한다고 보면 된다.
생성주기
- Bean 생성 단계
- Spring은 빈을 생성하기 위해 빈의 클래스를 로드하고, 해당 클래스의 인스턴스를 생성한다.
- 이 단계에서는 빈의 생성자가 호출되고, 객체가 메모리에 인스턴스화된다.
- 의존성 주입 단계
- 생성된 빈에게 해당하는 의존성들이 주입된다.
- 이를 통해 빈은 필요한 다른 빈들과 연결되고, 런타임에 객체 간의 관계가 형성된다.
- 초기화 단계:
- 모든 의존성이 주입된 후, 빈은 초기화 단계를 진행한다.
- 빈이 InitializingBean 인터페이스를 구현하거나, @PostConstruct 어노테이션이 지정된 메서드를 포함하면 해당 메서드가 호출된다.
- 이 단계에서 빈은 사용 가능한 상태가 된다.
- 사용 단계:
- 이 단계에서 빈은 실제 비즈니스 로직을 수행하거나, 다른 빈과 상호작용한다.
- 소멸 단계:
- 빈의 생명 주기가 끝나면, 빈은 컨테이너에서 제거된다.
- 빈이 DisposableBean 인터페이스를 구현하거나, @PreDestroy 어노테이션이 지정된 메서드를 포함하면 해당 메서드가 호출되어 마무리 작업을 수행한다.
위와 같은 빈의 생성 주기를 관리하기 위해 Spring은 빈 팩토리(Bean Factory) 또는 애플리케이션 컨텍스트(Application Context)를 사용한다.
**애플리케이션 컨텍스트는 빈 팩토리를 확장한 기능이며, 빈의 생성, 의존성 주입, 초기화, 소멸과 같은 빈의 생명 주기를 관리하는 책임을 갖고 있다.
애플리케이션 컨텍스트는 일반적으로 XML, 어노테이션 등을 사용하여 빈의 구성 정보를 제공받는다.
프로토타입 빈
프로토타입 빈은 Spring Framework에서 제공하는 빈의 스코프(scope) 중 하나이다.
빈의 스코프는 해당 빈이 생성되고 존재하는 범위를 정의하는 것을 의미.
프로토타입 빈은 요청할 때마다 매번 새로운 인스턴스가 생성되는 스코프 이다.
Spring의 빈은 기본적으로 싱글톤(Singleton) 스코프 로 생성된다.
싱글톤 스코프는 하나의 빈 인스턴스만을 생성하여 컨테이너에서 공유하기 때문에 동일한 빈을 여러 번 요청하더라도 매번 같은 인스턴스를 반환한다.
하지만 프로토타입 빈은 매번 요청할 때마다 새로운 인스턴스를 생성하여 반환한다.
즉, 매 요청마다 독립적인 상태를 유지해야 하는 경우 유용한데,
예를 들어 웹 애플리케이션에서 요청마다 새로운 폼 객체를 생성하거나, 상태 정보를 담은 객체를 사용해야 할 때 프로토타입 빈을 사용할 수 있다.
프로토타입 빈은 요청 시점에서 생성되고, 해당 빈이 더 이상 사용되지 않을 때 소멸된다.
이러한 동작은 역시 애플리케이션 컨텍스트 또는 빈 팩토리에 의해 관리된다.
2. AOP (Aspect-Oriented Programming)
AOP는 애플리케이션의 핵심 비즈니스 로직과 부가적인 기능을 분리하여 개발하는 방법을 말한다.
AOP는 애플리케이션의 여러 모듈에 걸쳐 공통적으로 적용되는 관심사를 분리하여 중복 코드를 최소화하고 코드의 재사용성과 유지보수성을 향상시키는데 사용된다.
공통되는 관심사는 예를 들면 이런 것들이 있을 수 있다.
- 로깅, 보안, 트랜잭션 처리
AOP는 이런 관심사를 모듈화하여 핵심 관심사와 분리시킨다.
- 스캐닝
- Spring은 컨테이너를 초기화하는 과정에서 @Aspect 어노테이션이 적용된 클래스를 우선 스캔한다.
- Pointcut 정의포인트컷은 어떤 메서드에 Advice를 적용할지를 선택하는 기준을 정의합니다.
- Aspect 클래스 내부에서 @Pointcut 어노테이션을 사용하여 특정 메서드를 포인트컷으로 정의한다.
- Advice 정의Advice는 횡단 관심사를 구현한 코드로, 언제(When)와 어떻게(How) 핵심 관심사에 적용할지를 정의한다.
- Aspect 클래스 내부에서 @Before, @After, @Around, @AfterReturning, @AfterThrowing 등의 어노테이션을 사용하여 Advice를 정의한다.
- Advisor 생성Advisor는 Advice와 Pointcut의 결합으로, 어떤 메서드에 어떤 Advice를 적용할지를 지정한다.
- Aspect 클래스 내부에서 @Pointcut과 @Before, @After, @Around, @AfterReturning, @AfterThrowing 등의 어노테이션을 조합하여 Advisor를 생성한다.
- 프록시 생성프록시는 핵심 비즈니스 로직의 메서드를 감싸는 래퍼(wrapper) 역할을 합니다.
- Spring은 Aspect에 정의된 Advisor를 기반으로 프록시를 생성한다.
- AOP 적용이를 통해 핵심 비즈니스 로직에 대한 수정 없이 공통 관심사를 추가할 수 있다.
- 프록시를 이용하여 공통 관심사를 핵심 비즈니스 로직에 적용한다.
정리하자면,
@Aspect 어노테이션을 사용하면 AOP 기능을 구현하는 데 매우 편리하고 가독성이 좋은 코드를 작성할 수 있다.
Aspect 클래스의 메서드는 Advice로서 공통 관심사를 구현하며, @Pointcut 어노테이션을 사용하여 메서드가 적용될 포인트컷을 정의한다.
Spring은 이러한 Aspect 클래스를 스캔하여 자동으로 AOP를 적용하며, 애플리케이션에 공통 관심사를 쉽게 추가할 수 있게 된다.
4회차 회고록
이번 회차에서는 드디어 스프링에 들어갔다. 스프링 프레임워크 학습에서 가장 중요하게 여긴 것은 IoC와 DI의 개념을 이해하는 것이었다. Spring이 제공하는 강력한 IoC 컨테이너를 통해 객체의 생성과 의존성 주입이 어떻게 관리되는지, 그리고 이를 통해 어떻게 느슨한 결합도와 높은 응집력을 가진 애플리케이션을 구축할 수 있는지에 대해 배웠다. DI를 통해 테스트 용이성과 코드 재사용성이 어떻게 향상되는지를 알 수 있어서 의미있었다. 이번 방학 동안 백엔드 기술에 대한 전반적인 복습을 할 수 있어서 정말 유익한 것 같다.
'[CNU] Mogakco' 카테고리의 다른 글
[2024_동계_모각코] 6회차(02/14) (1) 2024.02.18 [2024_동계_모각코] 5회차(01/31) (0) 2024.02.18 [2024_동계_모각코] 3회차(01/17) (0) 2024.02.18 [2024_동계_모각코] 2회차(01/12) (0) 2024.02.18 [2024_동계_모각코] 1회차(01/03) (0) 2024.01.03 - equals() 메서드