실무에서는 비즈니스 요구사항이 굉장히 자주 바뀌기 때문에 OCP와 DIP를 준수하는 것이 변경사항에 대비하기 유리하다고 한다.
* 아래 코드는OCP와 DIP를 위반한 예제 코드다.
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
실제 주문서비스를 기능하기 위한 OrderServiceImpl(구현체)는 OrderService(인터페이스)를 상속받아 생성된다.
이때, 할인 정책을 도입하기 위해 DIscountPolicy(인터페이스)에 의존관계를 가지며 private final을 통해 외부에서 고정된 값으로 실제 할인정책(구현체)를 사용한다.
DiscountPolicy가 자동차 역할, FixDiscountPolicy와 RateDiscountPolicy는 K3, 아반떼, 모델3라고 할 수 있다.
운전자는 어딘가로 이동하겠다는 목적을 위해 자동차를 이용하고자 한다. 이때 자동차를 타고 이동하는 것이 중요한 것이지 어떤 기종을 타는지는 목적을 이루는데 중요하지 않다.
그러나, 위 예제 코드에서는 K3를 타도록 지정되어 있는 것과 같다. - DIP 위반!
그리고 외부 사정으로 인해 K3를 이용하기 어려워 아반떼로 변경되는 상황에서 운전자에게 변경 정보를 입력해 아반떼를 찾아 타게 만들어야 하는 것이다. - OCP위반 !
Config 구성 클래스
운전자가 자동차 운전(실행)에만 집중할 수 있도록 자동차 설정은Config라는 구성클래스를 통해 따로 관리해야한다.
package hello.core.order;
import ...
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
package hello.core;
import ...
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
위 코드는 OrderService메서드에서 OrderServiceImpl 구현체를 생성하고 생성자 주입을 통해 FixDiscountPolicy 구현체를 주입하고 있다.
아래 코드는 수정된 OrderServiceImpl 클래스다. DiscountPolicy만 의존관계를 갖고 나머지는 생성자 주입을 통해 구현체를 관리한다.
OrderServiceImpl과 Appconfig 클래스는 각각 역할을 구분하여 책임을 부여하고 있다. 이를 통해 각 클래스가 어떤 역할을 하는지 명확하게 확인할 수 있고 변경사항에도 대비할 수 있다.
객체 지향 프로그래밍(영어:Object-Oriented Programming, OOP)은컴퓨터 프로그래밍의패러다임중 하나이다. 객체 지향 프로그래밍은컴퓨터 프로그램을명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는메시지를 주고받고,데이터를 처리할 수 있다.
객체 지향 프로그래밍은 프로그램을 유연하고 변경이 쉽게 만들기 때문에 대규모소프트웨어 개발에 많이 사용된다. 또한 프로그래밍을 더 배우기 쉽게 하고 소프트웨어 개발과 보수를 간편하게 하며, 보다 직관적인코드분석을 가능하게 하는 장점이 있다. 그러나 지나친 프로그램의 객체화 경향은 실제 세계의 모습을 그대로 반영하지 못한다는 비판을 받기도 한다.
또한, 자바 언어 설명서에 의하면 객체는 클래스 인스턴스 또는 배열로 정의된다.
An object is a class instance or an array.
처음 들었을 때는 객체, 클래스, 인스턴스 등 이해되지 않는 말들 투성이였다.
그렇지만 개발자 = 창조자(god)에 비유하면 이해를 도울 수 있으리라 생각한다. 자, 우리는 어플리케이션이라는 세상을 창조하려고 한다. 세상을 구성하고 기능하게 하기 위해서는 피조물(객체)가 필요하다.
객체는 추상적, 구체적 정의로 나뉜다. 아래 그림을 예로 들면, K3, 아반떼, 모델3는 모두 자동차면서도 서로 구분이 된다. 여기서 자동차는 추상적 정의다. 우리의 머리 속에만 존재하는 개념.
A : 나 '자동차' 살거야.
B : 어떤 자동차?
A : K3!
A와 B는 모두 자동차에 대해 이야기를 하고 있지만, 실제로 사용하기 위해서는 '어떤 자동차'라는 구체적인 정의가 필요하다.
다형성은 자동차라는 클래스를 수만가지의 종류를 구현할 수 있음을 의미한다.
프로그래밍 관점에서 클라이언트가 '자동차를 타고 싶어'라고 요청을 보냈다면 K3를 타다가 아반떼를 타고 그리고 모델3로 바꿔 타도 문제가 없다.
인터페이스와 상속
그래서 자동차는 단순히 공통분모로 정의되는 개념일까?
우리가 머리 속에 자동차를 떠올릴 때 생각나는 형태가 있을 것이다. 그 특징들의 차이로 K3, 아반떼, 모델3를 구분할 수 있다. 이 특징들은 매우 중요하고 이를 추상 메서드라고 한다.
추상 메서드는 추상 클래스와 인터페이스를 통해 상속되는 개념인데, 클래스는 하나의 추상 메서드만을 상속하지만, 인터페이스는 여러 개의 추상 메서드를 상속할 수 있다.
하위 클래스에서의 강제 구현:추상 메서드는 하위 클래스에서 반드시 구현되어야 하는 메서드입니다. 추상 클래스나 인터페이스에 있는 추상 메서드를 상속받은 클래스는 해당 메서드를 반드시 구현해야 합니다. 이를 통해 특정한 동작이나 기능을 하위 클래스에서 강제할 수 있습니다.
다형성 구현:추상 메서드는 다형성(polymorphism)을 구현하는데 사용됩니다. 여러 하위 클래스가 같은 추상 메서드를 각각의 방식으로 구현함으로써, 이들 객체를 동일한 추상 타입으로 다룰 수 있습니다.
클래스 설계의 기본 틀 제공:추상 클래스에서는 구현이 필요한 메서드를 제공하면서 동시에 기본적인 구조나 틀을 제공할 수 있습니다. 이를 상속받은 하위 클래스는 이러한 틀을 기반으로 추가적인 동작을 정의할 수 있습니다.
예를 들어, 동물을 나타내는 추상 클래스가 makeSound라는 추상 메서드를 가지고 있을 때, 이를 상속받은 구체적인 동물 클래스에서는 각 동물의 소리를 구현할 수 있습니다. 이로써 동물의 다양성을 표현하고 공통된 특징을 추상 클래스에서 제공할 수 있습니다.
*동물을 키워드로 아래 코드를 보면 이해가 쉬울 것이라 생각한다.
// 동물을 나타내는 인터페이스
interface Animal {
void makeSound(); // 추상 메서드: 동물의 울음 소리를 나타냄
}
// 각 동물을 구현하는 클래스
class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("멍멍!"); // 강아지는 멍멍 짖음
}
}
class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("야옹!"); // 고양이는 야옹 소리
}
}
class Duck implements Animal {
@Override
public void makeSound() {
System.out.println("꽥꽥!"); // 오리는 꽥꽥 소리
}
}
public class AnimalExample {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
Animal duck = new Duck();
dog.makeSound(); // 멍멍!
cat.makeSound(); // 야옹!
duck.makeSound(); // 꽥꽥!
}
}
인터페이스의 다중 구현
FlyingFish는 Fly와 Swim 두가지 추상 메서드를 상속 받는다.
// 인터페이스 정의
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
// 클래스가 두 인터페이스를 다중으로 구현
class FlyingFish implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("Flying in the air!");
}
@Override
public void swim() {
System.out.println("Swimming in the water!");
}
}
public class Main {
public static void main(String[] args) {
FlyingFish flyingFish = new FlyingFish();
flyingFish.fly(); // Flying in the air!
flyingFish.swim(); // Swimming in the water!
}
}
Model:데이터와 비즈니스 로직을 담당합니다. 데이터의 상태를 유지하고 업데이트하는 역할을 합니다.
View:사용자에게 보이는 부분을 담당합니다. Model의 데이터를 시각적으로 표현하고 사용자와의 상호작용을 처리합니다.
Controller:사용자의 입력을 받아 Model과 View 사이에서 중개자 역할을 합니다. 입력을 기반으로 Model을 업데이트하고, View를 갱신합니다.
장점:애플리케이션의 모듈화, 유지보수성, 확장성을 향상시킵니다.
*예시
@Controller
@RequestMapping("/example")
public class ExampleController {
// "/example/hello"에 대한 GET 요청을 처리하는 메서드
@GetMapping("/hello")
public String hello() {
return "helloPage";
}
// "/example/greet"에 대한 POST 요청을 처리하는 메서드
@PostMapping("/greet")
public String greet(@RequestParam String name, Model model) {
model.addAttribute("name", name);
return "greetPage";
}
}
@Controller라는 어노테이션을 통해 위의 클래스가 컨트롤러임을 지정한다.
@___Mapping은 요청에 따라 메서드를 정하고 return 값을 통해 반환될 View를 지정한다.
자세한 내용은 HTTP 정리 글에서 다룰 예정이므로 간략하게 Mapping의 종류만 정리한다.
@GetMapping("/example")
public String example() {
// ...
}
@PostMapping("/create")
public String create(@RequestBody DataObject data) {
// ...
}
@PutMapping("/update/{id}")
public String update(@PathVariable Long id, @RequestBody UpdatedDataObject updatedData) {
// ...
}
@DeleteMapping("/delete/{id}")
public String delete(@PathVariable Long id) {
// ...
}
API
백엔드 개발자의 주업무라고 해야하나.. 회원, 상품, 주문, 정산 등 API개발을 한다.
하나의 소프트웨어는 컨포넌트의 집합이라고 할 수 있다. 그리고 컨포넌트 간의 통신을 돕는 것이 API의 역할이다.
동작: 주로 비즈니스 로직을 구현하고, 해당 기능에 필요한 데이터를 처리하기 위해 리포지토리를 호출합니다.
리포지토리 (Repository):
역할: 데이터베이스와 관련된 작업을 수행하며, 데이터에 접근하고 변경하는 역할을 합니다.
동작: 데이터의 CRUD(Create, Read, Update, Delete) 작업을 처리하고, 서비스에게 필요한 데이터를 제공합니다.
컨트롤러 (Controller):
역할: 사용자의 요청을 받고, 해당 요청에 대한 응답을 반환하는 부분입니다. 웹 애플리케이션에서 클라이언트와 상호작용합니다.
동작: HTTP 요청을 받아서 서비스에게 전달하고, 서비스로부터 받은 결과를 적절한 형태로 응답합니다.
*예시
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void registerMember(Member member) {
// 비즈니스 로직 수행, 예를 들어 중복 회원 검사 등
if (isDuplicateMember(member)) {
throw new IllegalStateException("이미 가입된 회원입니다.");
}
memberRepository.save(member);
}
private boolean isDuplicateMember(Member member) {
// 중복 여부 검사 로직
}
}
@Repository
public class MemberRepository {
private final Map<Long, Member> store = new HashMap<>();
private long sequence = 0L;
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping("/members/{id}")
public String getMemberById(@PathVariable Long id, Model model) {
// 회원 조회 로직
Member member = memberService.getMemberById(id);
model.addAttribute("member", member);
return "memberDetail";
}
@PostMapping("/members/new")
public String registerNewMember(@ModelAttribute MemberForm form) {
// 회원 가입 로직
Member member = new Member();
member.setName(form.getName());
memberService.registerMember(member);
return "redirect:/members/" + member.getId();
}
}
Spring Data JPA에서는 JpaRepository 인터페이스를 확장하여 Repository를 생성할 때 이미 기본적인 CRUD 메서드들을 제공받게 됩니다. 개발자는 이러한 메서드를 사용함으로써 별도의 쿼리 메서드 작성이나 엔터티 매니저를 직접 다루지 않아도 됩니다. Spring Data JPA는 이러한 구현체를 런타임에 자동으로 생성하여 빈으로 등록합니다.
JPA와 Spring Data JPA 차이
JPA 예시:
//엔터티 클래스 (Entity Class):
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters, setters, 기타 메서드
}
//JPA를 사용한 Repository:
import javax.persistence.EntityManager;
import java.util.List;
public class MemberRepository {
private final EntityManager em;
public MemberRepository(EntityManager em) {
this.em = em;
}
public void save(Member member) {
em.persist(member);
}
public Member findById(Long id) {
return em.find(Member.class, id);
}
public List<Member> findAll() {
return em.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
}
// 기타 필요한 메서드들
}
//엔터티 매니저 활용:
EntityManager em = // 엔터티 매니저 생성 (예: EntityManagerFactory로부터)
MemberRepository repository = new MemberRepository(em);
Member member = new Member();
member.setName("John Doe");
repository.save(member);
Member foundMember = repository.findById(member.getId());
List<Member> allMembers = repository.findAll();
Spring Data JPA 예시:
//엔터티는 동일
//Spring Data JPA Repository:
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
// JpaRepository에서 기본적인 CRUD 메서드들을 이미 제공받음
}
//Spring Data JPA 활용:
MemberRepository repository = // Spring이 자동으로 구현한 Repository 빈을 주입받음
Member member = new Member();
member.setName("John Doe");
repository.save(member);
Member foundMember = repository.findById(member.getId()).orElse(null);
List<Member> allMembers = repository.findAll();
@Bean 등록 - Config
@Configuration을 가진 클래스는 주로 @Bean을 등록해 DI(의존성 주입)을 한다.