Dependency Injection (DI)은 객체 지향 프로그래밍에서 사용되는 디자인 패턴 중 하나로, 클래스 간의 결합도를 낮추고 코드의 유연성과 재사용성을 높이는데 도움을 준다. DI는 클래스 내부에서 직접적으로 객체를 생성하는 대신, 외부에서 생성된 객체(의존성)를 주입하는 방식으로 작동한다.

 

이 패턴의 주요 이점은 다음과 같다:

 

① 코드의 모듈성: 의존성 주입을 사용하면 클래스들이 서로 덜 의존하게 되므로, 코드의 모듈성이 향상된다.

② 유연성과 확장성: 클래스 간 결합도가 낮아지면, 기능 변경이나 확장이 더 쉽고 간단해진다.

③ 테스트 용이성: 의존성 주입을 사용하면, 테스트 시 의존성을 쉽게 대체하거나 모의 객체로 교체할 수 있어 테스트 용이성이 향상된다.

DI는 주로 생성자 주입, 세터 주입, 인터페이스 주입 등의 방식으로 구현할 수 있다. 프레임워크나 라이브러리를 통해 자동화된 의존성 주입을 사용할 수도 있다. 대표적인 예로 Java의 Spring 프레임워크, .NET의 ASP.NET Core, Python의 Flask-Injector 등이 있다.

 

예시코드


  1. MessageService Interface를 만든다.
public interface MessageService {
  String getMessage();
}
  1. `MessageService` 인터페이스를 구현한 `HelloMessageService` 클래스를 생성한다.
import org.springframework.stereotype.Service;

@Service
public class HelloMessageService implements MessageService {
  @Override
  public String getMessage() {
    return "Hello, Spring Boot!"
  }
}
  1. `MessageController` 클래스를 생성하고, `MessageService`를 의존성을 주입한다.

이때 @Autowired 사용한다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageController {
  private final MessageService messageService;
  
  @Autowired
  public MessageController(MessageService messageService) {
    this.messageService = messageService;
  }
  
  @GetMapping("/message")
  public String getMessage() {
    return messageService.getMessage();
  }
}

위 코드에서 `MessageController`의 생성자에 `@Autowired` 사용하여

`MessageService`의 구현체인 `HelloMessageService`를 자동으로 주입한다.

이렇게 하면 `MessageController`는 `MessageService`의 구체적인 구현에 대해 알 필요 없이 인터페이스를 통해 메서드를 호출할 수 있다. 이 방식은 코드의 결합도를 낮추고 유연성을 높이는 데 도움을 준다.

만약 MessageService 인터페이스의 구현체를 바꾸고 @Autowired에 되는 걸 바꾸고 싶다면,

1.일단 MessageService 인터페이스의 구현체를 하나 더 만든다.

import org.springframework.stereotype.Service;

@Service
public class GoodbyeMessageService implements MessageService {
  @Override
  public String getMessage() {
    return "Goodbye, Spring Boot!"
  }
  
}

2.MessageConfiguration 설정 클래스를 하나 만들어서 GoodbyeMessageService를 사용하도록 설정한다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MessageConfiguration {
  
  @Bean
  public MessageService messageService() {
    return new GoodbyeMessageService();
  }
}

위의 코드에서 `MessageConfiguration` 클래스를 사용해서

`MessageService` 빈을 `GoodbyeMessageService`의 인스턴스로 설정했다.

이제 `MessageController`에서 `MessageService`인터페이스의 구현체로

`GoodbyeMessageService`가 주입되어 사용된다.

데이터 전송을 처리하는 프로그램 작성 시, 중요한 개념 중 하나는 DTO(Data Transfer Object)이다. Java에서의 DTO 개념과 사용 이유에 대해 설명한다.

 

DTO는 보통 데이터베이스의 각 테이블에서 가져온 데이터를 담아서 사용한다. 이를 통해 데이터를 전송하는 과정에서 데이터를 캡슐화하고, 비즈니스 로직과 데이터베이스 간의 데이터 교환을 단순화한다.

DTO를 사용하는 이유는 데이터베이스와 비즈니스 로직 간의 결합도를 낮추기 위함이다. 즉, 데이터베이스의 변경에 영향을 받지 않고 비즈니스 로직을 수정할 수 있도록 해주는 것이다. 또한 DTO를 사용하면 데이터 전송 시 데이터의 무결성을 보장할 수 있다.

간단히 말해, DTO는 데이터 전송에 필요한 객체로, 데이터베이스나 외부 시스템에서 가져온 데이터를 비즈니스 로직에서 사용할 수 있는 형태로 변환해주는 역할을 한다. 이를 통해 데이터의 무결성을 보장하고, 데이터베이스와 비즈니스 로직 간의 결합도를 낮출 수 있다.

예를 들어, 고객 정보를 데이터베이스에서 가져와서 비즈니스 로직에서 사용해야 하는 경우를 생각해보겠다.

데이터베이스에서는 고객 정보를 다음과 같은 형태로 저장할 수 있다.

Customer table

-----------------------------

| ID | Name | Age | Address |

-----------------------------

| 01 | Tom | 30 | Seoul |

| 02 | Jane | 25 | Busan |

-----------------------------

 

이때, 고객 정보를 비즈니스 로직에서 사용하기 위해서는 데이터베이스의 테이블과는 다른 형태로 변환해야 한다. 이때 DTO를 사용하면 다음과 같이 변환할 수 있다.

public class CustomerDTO {
    private String id;
    private String name;
    private int age;
    private String address;

    public CustomerDTO(String id, String name, int age, String address) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }
}

위와 같이 DTO 클래스를 작성하면, 데이터베이스에서 가져온 고객 정보를 DTO 객체에 담아서 비즈니스 로직에서 사용할 수 있다. 이를 통해 데이터베이스와 비즈니스 로직 간의 결합도를 낮출 수 있다.

데이터베이스와 연동하여 데이터를 조작하는 프로그램 작성 시, 중요한 개념 중 하나는 DAO(Data Access Object)이다.  Java에서의 DAO 개념과 사용 이유에 대해 설명하겠다.

 

Java에서 DAO는 Data Access Object의 약어로, 데이터베이스와 연동하여 데이터를 조작하는 객체를 말한다.

DAO는 데이터베이스와의 연결을 관리하고, 데이터베이스에서 데이터를 가져오거나 데이터를 저장하는 등의 역할을 수행한다. DAO는 보통 인터페이스로 작성되어 있고, 실제로는 이 인터페이스를 구현하는 클래스를 사용한다.

DAO는 데이터베이스와 비즈니스 로직 간의 결합도를 낮추는데 큰 역할을 한다. 데이터베이스가 변경되어도 비즈니스 로직이 영향을 받지 않도록 해주며, 코드의 재사용성과 유지보수성을 높여준다.

DAO를 사용하면 데이터베이스와의 연결과 데이터베이스 조작을 캡슐화할 수 있으므로, 비즈니스 로직에서는 단순히 DAO의 메서드를 호출함으로써 데이터베이스 조작을 수행할 수 있다. 이를 통해 코드의 가독성과 유지보수성을 높일 수 있다.

간단히 말해, DAO는 데이터베이스와 연동하여 데이터를 조작하는 객체로, 데이터베이스와 비즈니스 로직 간의 결합도를 낮추고 코드의 재사용성과 유지보수성을 높여주는 것이다.

예를 들어, 사용자 정보를 데이터베이스에서 가져오는 기능을 DAO를 사용하여 구현하는 예를 생각해 보겠다.

데이터베이스에서는 사용자 정보를 다음과 같은 형태로 저장할 수 있다.

User table

------------------------

| ID | Name | Address |

------------------------

| 01 | Tom | Seoul |

| 02 | Jane | Busan |

| 03 | Alice | Daegu |

------------------------

이때, 사용자 정보를 데이터베이스에서 가져와서 비즈니스 로직에서 사용하기 위해서는 DAO를 사용하여 다음과 같이 구현할 수 있다.

public interface UserDao {
    List getAllUsers();
    User getUserById(String id);
}

public class UserDaoImpl implements UserDao {
    private Connection conn;

    public UserDaoImpl(Connection conn) {
        this.conn = conn;
    }

    @Override
    public List getAllUsers() {
        List users = new ArrayList<>();
        try (Statement stmt = conn.createStatement()) {
            String query = "SELECT * FROM User";
            ResultSet rs = stmt.executeQuery(query);
            while (rs.next()) {
                String id = rs.getString("ID");
                String name = rs.getString("Name");
                String address = rs.getString("Address");
                User user = new User(id, name, address);
                users.add(user);
            }
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
        return users;
    }

    @Override
    public User getUserById(String id) {
        User user = null;
        try (PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM User WHERE ID = ?")) {
            pstmt.setString(1, id);
            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                String name = rs.getString("Name");
                String address = rs.getString("Address");
                user = new User(id, name, address);
            }
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
        return user;
    }
}

위와 같이 UserDao 인터페이스를 정의하고, UserDaoImpl 클래스를 구현하여 DAO를 구현할 수 있다. UserDaoImpl 클래스에서는 Connection 객체를 사용하여 데이터베이스와 연결하고, getAllUsers() 메서드와 getUserById() 메서드를 구현하여 사용자 정보를 가져올 수 있다. 이를 통해 데이터베이스와 비즈니스 로직 간의 결합도를 낮추고, 코드의 재사용성과 유지보수성을 높일 수 있는 것이다.

DTO: Data Transfer Object

DAO: Data Access Object

DAO와 DTO는 둘 다 데이터베이스와 연관된 기능을 수행하는 객체이지만, 각각의 역할과 목적이

다르다.


DAO는 데이터베이스와 연결을 관리하고, 데이터베이스에서 데이터를 가져오거나 데이터를 저장하는 등의 역할을 수행한다. 즉, DAO는 데이터베이스와의 연결과 데이터베이스 조작을 캡슐화하는 객체이다.

"조작을 캡슐화한다"는 말은, 데이터베이스와의 조작을 한 곳에 모아서 캡슐화하는 것을 의미. 이것은 다시 말해, 데이터베이스와의 연결을 추상화하고, 데이터베이스에서 데이터를 가져오는 방법과 저장하는 방법을 명확하게 구분하는 것.

예를 들어, DAO를 사용하지 않고 직접 데이터베이스와 연결하여 SQL 쿼리를 작성하고 데이터를 가져오는 코드를 작성한다면, 코드가 복잡해지고 유지보수가 어려워지는데, 이 때, DAO를 사용하면 데이터베이스 연결과 SQL 쿼리 작성 등을 한 곳에서 캡슐화하므로, 코드가 간결해지고 유지보수가 쉬워진다.

반면, DTO는 데이터를 전송하는데 사용되는 객체로, 데이터베이스나 외부 시스템에서 가져온 데이터를 비즈니스 로직에서 사용하기 쉽게 변환해주는 역할을 한다. DTO는 데이터를 전송하는 과정에서 데이터를 캡슐화하고, 비즈니스 로직과 데이터베이스 간의 데이터 교환을 단순화한다.

따라서 DAO와 DTO는 각각의 역할과 목적이 다르다. DAO는 데이터베이스와의 연결과 데이터베이스 조작을 캡슐화하며, DTO는 데이터 전송을 캡슐화한다. DAO와 DTO는 데이터베이스와 연동하는 기능을 구현하는 데 사용되는 객체이지만, 다른 방식으로 된다.


사용자가 로그인을 시도하면 클라이언트는 사용자가 입력한 정보를 서버로 전송하고, 서버는 해당 정보를 검증하여 로그인 결과를 클라이언트로 전송한다. 이때 DTO는 클라이언트와 서버 간데이터 전송을 캡슐화하는 데 사용된다.

또한, DTO는 서버 사이드에서도 사용될 수 있다. 예를 들어, 서버 사이드에서 데이터베이스에서 가져온 정보를 DTO로 변환하여 비즈니스 로직으로 전달할 수 있다. 이를 통해 비즈니스 로직에서 데이터베이스와의 결합도를 낮추고, 데이터 전송 시 데이터의 무결성을 보장할 수 있다.


DAO로 데이터베이스 연결을 한다면, 일반적으로 최초 1회에 한해 연결이 수행된다. 이후 DAO 객체를 재사용하면 이미 연결된 데이터베이스와 계속해서 작업할 수 있다.

하지만, 연결된 데이터베이스와 일정 시간 이상 연결이 유지되지 않으면 연결이 끊어지는 경우도 있다. 이 경우 DAO 객체를 다시 사용하기 전에 데이터베이스와 연결을 다시 수행해야 한다.

DAO는 데이터베이스와 연결하여 데이터를 조작하는 기능을 담당한다. 이를 통해 비즈니스 로직에서 데이터베이스와의 결합도를 낮추고, 코드의 재사용성과 유지보수성을 높일 수 있다.

 

DAO로 데이터베이스를 조작하는 방법은 다양하다. 보통 "JDBC(Java Database Connectivity) API"를 사용하여 데이터베이스와의 연결 및 데이터 조작을 수행한다.

 

JDBC는 Java에서 데이터베이스와의 연결을 위한 표준 API로, 다양한 데이터베이스와 호환된다.

DAO에서는 JDBC API를 사용하여 SQL문을 작성하고, 이를 데이터베이스에 전달하여 데이터를 조작한다. SQL문은 SELECT, INSERT, UPDATE, DELETE 등의 다양한 종류가 있으며, 데이터베이스와 상황에 따라 적절한 SQL문을 사용하여 데이터를 처리한다.

또한, DAO에서는 JDBC API를 사용하여 데이터베이스 연결 및 연결 해제, 데이터 삽입, 수정, 삭제 등의 기능을 수행한다. 이러한 기능을 캡슐화하여 비즈니스 로직에서는 DAO의 메서드를 호출함으로써 데이터베이스 조작을 수행할 수 있다.

 

따라서, DAO는 JDBC API를 사용하여 데이터베이스와 연결하고, SQL문을 작성하여 데이터를 조작하고. 이를 통해 데이터베이스와 비즈니스 로직 간의 결합도를 낮추고, 코드의 재사용성과 유지보수성을 높일 수 있는 친구다.

Spring boot는 스프링 기반으로 실무 환경에 사용 가능한 수준의 '독립실행형' Application을 복잡한 고민 없이, 빠르게 작성할 수 있게 도와주는 여러가지 도구의 모음이다.

스프링 ≠ 스프링부트 !

Spring Boot 핵심 목표

ⓐ 매우 빠르고 광범위한 영역의 스프링 개발 경험을 제공

ⓑ 강한 주장을 가지고, 즉시 적용 가능한 기술 조합을 제공하면서, 필요에 따라 원하는 방식으로 손쉽게 변형 가능

ⓒ Project에서 필요로 하는 다양한 비기능적인 기술(내장형 서버,보안,메트릭,상태 체크,외부 설정 방식 등) 제공

ⓓ코드 생성이나 XML 설정 필요하지 않음.

@Bean Annotation @Configuration @Autowired

"Bean": 스프링 프레임워크에서 관리하는 객체

@Configuration : 빈(bean) 정의 및 의존성 주입 등 다양한 설정을 포함할 수 있다.

1.빈 등록: @Bean 어노테이션과 함께 사용하여 스프링 컨테이너에 빈을 등록. 이를 통해 다른 클래스에서 필요할 때 주입하여 사용할 수 있다.

2.의존성 주입: @Autowired나 @Inject와 같은 어노테이션을 사용하여 다른 빈들 간의 의존성을 주입한다.

3.환경 설정: @PropertySource 어노테이션을 사용하여 외부 설정 파일을 읽어들일 수 있다.

4.프로필 설정: @Profile 어노테이션을 사용하여 개발, 테스트, 운영 등 다양한 환경에 맞는 빈 설정을 구성할 수 있다.

//svc interface
public interface MyService {
    String getMessage();
}
//svc Impl
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class MyServiceImpl implements MyService {

    private final MyRepository myRepository;

    @Override
    public String getMessage() {
        return myRepository.getMessage();
    }
}
//DAO interface
public interface MyRepository {
    String getMessage();
}
//DAO Impl
public class MyRepositoryImpl implements MyRepository {

    @Override
    public String getMessage() {
        return "Hello from MyRepositoryImpl";
    }
}
@Configuration
public class AppConfig {

    @Bean
    public MyService myService(MyRepository myRepository) {
        return new MyServiceImpl(myRepository);
    }

    @Bean
    public MyRepository myRepository() {
        return new MyRepositoryImpl();
    }
}
//Controller
@RestController
public class MyController {

    private final MyService myService;

    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }

    @GetMapping("/message")
    public String message() {
        return myService.getMessage();
    }
}

오늘은 객체 지향 프로그래밍의 세계에서 데이터베이스와 매핑되는 클래스, 즉 엔티티 클래스(Entity Class)에 대해 알아보겠다. 누군가는 엔티티 클래스를 객체와 데이터베이스 사이의 로맨틱한 브리지라고 불렀다고 한다. 그러면 이 로맨스가 어떻게 이루어지는지 함께 살펴보자.

 

JPA에서의 엔티티 클래스
JPA(Java Persistence API)에서는 엔티티 클래스를 사용하여 데이터베이스 테이블과 매핑을 수행한다. 이렇게 함으로써, JPA는 객체와 데이터베이스 간의 변환을 자동으로 수행할 수 있다.

이름은 좀 어렵게 들릴 수 있지만, 사실 엔티티 클래스는 마치 영화에서 주인공과 그의 사랑하는 사람 사이에 서는 코미디 캐릭터 같은 역할을 한다. 그들 사이를 이어주는 역할이라고 생각하면 된다!

다음과 같은 엔티티 클래스를 살펴보겠다.

@Entity
@Table(name = "product")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "price")
    private int price;

    // ... 생성자, getter, setter 등의 메서드
}

위 코드에서는 @Entity 어노테이션을 사용하여 Product 클래스를 엔티티 클래스로 지정하고 있다. @Table 어노테이션을 사용하여 해당 엔티티가 매핑되는 데이터베이스의 테이블을 지정하고, @Id, @GeneratedValue, @Column 어노테이션을 사용하여 해당 엔티티의 필드와 데이터베이스 테이블의 컬럼을 매핑하고 있다.

이제 이 로맨스를 좀 더 편하게 즐길 수 있는 방법에 대해 알아보자.

 

스프링 부트와 JPA의 환상적인 만남


스프링 부트를 사용하면 JPA의 트랜잭션 처리를 더욱 쉽게 할 수 있다. 스프링 부트는 스프링 프레임워크의 기능을 확장하여 개발자들이 보다

위 코드에서는 ProductService 클래스의 updateProductName 메서드에 @Transactional 애노테이션을 추가하여, 해당 메서드에서 수행되는 모든 데이터 변경 작업이 하나의 트랜잭션으로 처리됨을 보장한다. 이렇게 함으로써, 트랜잭션 처리를 보다 간편하게 할 수 있다.

 

트랜잭션 처리 간편화
스프링 부트에서 JPA를 사용할 때는, 트랜잭션 처리를 위한 코드를 별도로 작성할 필요가 없다. 스프링 프레임워크에서 제공하는 @Transactional 애노테이션을 사용하여 간단하게 트랜잭션 처리를 할 수 있다.

예를 들어, ProductService 클래스에서 Product 엔티티를 저장하는 메서드를 작성한다고 가정해 보겠다.

@Service
public class ProductService {
    @Autowired
    private EntityManager entityManager;

    @Transactional
    public void saveProduct(String name, int price) {
        Product product = new Product();
        product.setName(name);
        product.setPrice(price);

        entityManager.persist(product);
    }
}

 

여기서 @Transactional 애노테이션을 사용하여 saveProduct 메서드에 트랜잭션 처리를 적용하고 있다. 이제 이 메서드를 호출하면, 스프링 프레임워크가 자동으로 트랜잭션을 시작하고, 메서드가 정상적으로 종료되면 트랜잭션을 커밋하게 된다. 만약 메서드 실행 중 예외가 발생하면, 트랜잭션은 자동으로 롤백된다.

레포지토리 사용의 편리성
또한 스프링 부트에서는 JpaRepository 인터페이스를 상속받는 레포지토리 클래스를 생성하여, 데이터베이스 작업을 보다 쉽게 처리할 수 있다. JpaRepository 인터페이스는 기본적인 CRUD 작업을 추상화하고 있어, 별도의 구현 없이도 사용할 수 있다.

public interface ProductRepository extends JpaRepository<Product, Long> {
}

이렇게 ProductRepository 인터페이스를 생성한 후, ProductService 클래스에서 이를 사용하면 된다.

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    public void saveProduct(String name, int price) {
        Product product = new Product();
        product.setName(name);
        product.setPrice(price);

        productRepository.save(product);
    }
}

이제 ProductService 클래스에서 JpaRepository를 통해 제공되는 기본 CRUD 메서드를 사용하여 데이터베이스 작업을 쉽게 처리할 수 있다.

정리하자면, 엔티티 클래스는 객체와 데이터베이스 사이의 로맨스를 이루는 중요한 요소입니다. 스프링 부트를 사용하면 JPA를 통해 엔티티 클래스와 데이터베이스 작업을 더욱 쉽게 처리할 수 있다. 트랜잭션 처리를 간편하게 적용할 수 있는 @Transactional 애노테이션과 JpaRepository 인터페이스를 사용하면, 코드의 가독성과 유지보수성이 높아진다.

 

 

+ Recent posts