In-Memory Database Practice

Seunghwan Choi·2024년 11월 28일

Java Backend

목록 보기
10/16

Dependency Injection

Dependency injection is a design pattern used to achieve loose coupling between classes by providing them with their dependencies instead of hardcoding them.

Constructor Injection

  • Dependencies are injected into a class through its constructor.
  • The required dependencies are passed as parameters in the constructor. Spring automatically detects and provides the required beans when the class is instantiated.
@Service
public class UserService {
	private final UserRepository userRepository;
    
    //Constructor Injection
    public UserService(UserRepository userRepository){
    	this.userRepository = userRepository;
    }
}
  • Advantages:
    - Immutability: Dependencies are marked as final, ensuring they can't be reassigned after construction
    - Testability: Easy to test because the constructor explicitly declares what the class depends on.
    - Safety: Guarantees that the required dependencies are present at the time of object creation, avoiding potential NullPointerException.
  • Disadvantage: Boilerplate code if there are multiple dependencies, though this can be mitigated with Lombok.

    Use when building production-grade applications where maintainability and testability are important. Recommended for all new projects.

Using @RequiredArgsConstructor

  • @RequiredArgsConstructor is a Lombok annotation that generates a constructor for all final fields.
  • When combined with DI in Spring, @RequiredArgsConstructor simplifies constructor injection by auto-generating the required constructor.
@Service
@RequiredArgsConstructor
public class UserService {
	private final UserRepository userRepository;
}

  • The above code automatically generated BookService(BookRepository) constructor as shown in the screenshot below:
  • Advantages:
    - Reduces Boilerplate: Eliminates the need to write repetitive constructor code
    - Immutability and Safety: Like constructor injection, dependencies are final.

    Use when we are comfortable with Lombok and want to reduce boilerplate while adhering to best practices.

Using @Autowired (Field injection)

  • @Autowired is a spring annotation that injects dependencies directly into fields.
  • Spring resolves and injects the dependency into the annotated field at runtime.
@Service
public class UserService {
	@Autowired
    private UserRepository userRepository;
    
    public void someMethod(){
    	userRepository.doSomething();
    }
}
  • Advantages:
    - Simpler Syntax: No need to explicitly write a constructor or use Lombok.
  • Disadvantages:
    - Less testable: Dependencies are not explicitly declared, making it harder to mock or replace them in tests.
    - Null Safety Issues: Dependencies are not final, making it possible to accidentally reassign them.

    Avoid in most cases, but it might be useful for rapid prototyping or small projects where simplicity is prioritized over maintainability.

Controller, Service, Repository

In a layered architecture, the Controller, Service, and Repository components correspond to specific layers in the application, each with distinct responsibilities. This design pattern helps in achieving separation of concerns, maintainability, and scalability.

Controller Layer

Responsibilities:

  • Map HTTP requests to handler methods using annotations like @RequestMapping, GetMapping.
  • Accept user input, often in JSON format, and delegate processing to the Service layer.
  • Validate user input using annotations like @Valid and handle validation errors.
  • Return responses (e.g. JSON) to clients.

Characteristics:

  • Should maintain thin, containing minimal logic
  • Focused solely on communication between the client and the service layer.

Service Layer

Responsibilities:

  • Encapsulate the application's core business rules.
  • Coordinate multiple data access methods (e.g. from different repositories)
  • Perform additional validation or transformation of data if needed
  • Handle exceptions and manage transactions using @Transactional.

Characteristics:

  • Should remain independent of the Controller and Repository layers for better reusability.
  • May contain reusable business logic that serves multiple Controllers or APIs

Repository Layer

Responsibilities:

  • Perform CRUD (Create, Read, Update, Delete) operations on data
  • Abstract database queries using Spring Data JPA, Hibernate, JDBC etc.
  • Translate database rows into application-friendly entities (e.g. User object)

Characteristics

  • Should only focus on data access, keeping the logic simple and reusable.
  • Works closely with the data persistence layer.

Layer Relationships

  1. Controller -> Service -> Repository:
    • The Controller receives a client request, validates it, and delegates the request to the Service.
    • The Service processes the request, applying business rules, and interacts with the Repository to fetch or persist data.
    • The Repository performs data access operations, querying or updating the database.
  2. Repository -> Service -> Controller:
    • The Repository returns the data to the Service, which processes it and returns a response to the Controller.
    • The Controller formats the response and sends it back to the client.

Example Interaction

  • Retrieve a user by ID:
  1. Controller Layer
    • Endpoint /api/users/{id} receives a GET request.
    • Calls userService.findUserById(id).
  2. Service Layer:
    • Calls userRepository.findById(id).
    • Applies any business rules (e.g. if the user is inactive, throw an exception)
  3. Repository Layer:
    • Executes a database query (SELECT * FROM users WHERE id = ?)
    • Returns the result (e.g. a User entity) to the Service layer.
  4. Controller Layer:
    • Converts the User entity into a JSON response and sends it back to the client.

Example (In-memory DB application)

Entity

  • We declare an abstract class that implements a PrimaryKey interface.
public abstract class Entity implements PrimaryKey{
    @Getter
    @Setter
    private Long id;
}
  • PrimaryKey Interface:
public interface PrimaryKey {

    void setId(Long id);

    Long getId();
}
  • The UserEntity extends Entity with additional fields specific to users:
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserEntity extends Entity {
    private String name;
    private int score;
}

Repository

  • We have a Repository interface:
public interface Repository<T, ID> {}
  • It is a marker interface in the context of layered architecture, typically found in data access layers. It represents a generic repository abstraction for working with data entities.
  • T is the type of entity (e.g. User, Order)
  • ID is the type of identifier for the entity (e.g. Long, String)
  • This is an empty interface, meaning it has no methods or behaviour defined directly. Its purpose is primarily semantic:
    - Indicates that any interface or class extending it is part of the repository layer.
  • We also have DataRepository:
public interface DataRepository<T, ID> extends Repository<T, ID> {

    //create, update
    T save(T data);

    //read
    Optional<T> findById(ID id);

    List<T> findAll();

    //delete
    void delete(ID id);
}
  • It is a generic repository abstraction that extends the marker interface Repository<T, ID>. It provides CRUD operations for managing data entities. The generic parameters allow it to work with any type of entity(T) and identifier(ID).

  • We then create an abstract class SimpleDataRepository that extends from DataRepository

public abstract class SimpleDataRepository<T extends Entity, ID extends Long> implements DataRepository<T, ID>{

    private List<T> dataList = new ArrayList<>();

    private static long index = 0;

    private Comparator<T> sort = new Comparator<T>() {
        @Override
        public int compare(T o1, T o2) {
            return Long.compare(o1.getId(), o2.getId());
        }
    };

    //create
    @Override
    public T save(T data){

        if(Objects.isNull(data)){
            throw new RuntimeException("Data is null");
        }

        var prevData = dataList.stream()
                .filter(it -> {
                    return it.getId().equals(data.getId());
                })
                .findFirst();

        if(prevData.isPresent()){
            dataList.remove(prevData.get());
            dataList.add(data);

        } else {
            index++;
            data.setId(index);
            dataList.add(data);
        }

        return null;
    }


    //read
    @Override
    public Optional<T> findById(ID id){
        return dataList.stream()
                .filter(it -> {
                    return it.getId().equals(id);
                })
                .findFirst();
    }

    @Override
    public List<T> findAll() {
        return dataList.stream()
                .sorted(sort)
                .collect(Collectors.toList());
    }

    //delete

    @Override
    public void delete(ID id) {
        var deleteEntity = dataList.stream()
                .filter(it -> {
                    return it.getId().equals(id);
                })
                .findFirst();

        if(deleteEntity.isPresent()){
            dataList.remove(deleteEntity.get());
        }
    }
}
  • This is an abstract class because it provides general CRUD logic but leaves certain specifics (e.g. the actual data entity structure) to subclasses. (Does not define how the Entity class or its structure should look like). Hence, specific repositories (e.g. UserRepository) need to extend it and specify the actual entity type and its behavior.

  • T extends Entity specifies that T must be a subclass of Entity, ensuring that every data entity has a consistent structure, like having an ID field.

  • ID extends Long indicates that the identifier type must be a Long.

  • The UserRepository:

@Slf4j
@Repository
public class UserRepository extends SimpleDataRepository<UserEntity, Long> {

    public List<UserEntity> findAllScoreGreaterThan(int score){

        return this.findAll().stream()
                .filter(
                        it -> {
                            return it.getScore() >= score;
                        }
                ).collect(Collectors.toList());
    }
}
  • It extends SimpleDataRepository to get working implementation of basic CRUD operations for UserEntity. This avoids rewriting common logic for saving, finding, or deleting records. Additional methods likefindAllScoreGreaterThan and addMultiple allow the repository to perform domain-specific (user-specific) operations on the data.

Service

@Service
//@RequiredArgsConstructor
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public UserEntity save(UserEntity user){
        return userRepository.save(user);
    }

    public List<UserEntity> findAll(){
        return userRepository.findAll();
    }

    public void delete(Long id){
        userRepository.delete(id);
    }

    public Optional<UserEntity> findById(Long id){
        return userRepository.findById(id);
    }

    public List<UserEntity> filterScore(int score){
        return userRepository.findAllScoreGreaterThan(score);
    }

    public void addall(List<UserEntity> userList){
        userRepository.addMultiple(userList);
    }
}
  • The @Autowired injects the UserRepository bean into the UserService. This connects the service layer with the repository layer. This is an alternative to using @RequiredArgsConstructor.

Controller

@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserApiController {

    private final UserService userService;

    @PutMapping("")
    public UserEntity create(
            //원래는 컨트롤러에서 받으면 안됨
            @RequestBody UserEntity userEntity
    ){
        return userService.save(userEntity);
    }

    @GetMapping("/all")
    public List<UserEntity> findAll(){
        return userService.findAll();
    }

    @DeleteMapping("/id/{id}")
    public void delete(
            @PathVariable Long id
    ){
        userService.delete(id);
    }

    @GetMapping("/id/{id}")
    public UserEntity findOne(
            @PathVariable Long id
    ) {
        var res = userService.findById(id);
        return res.get();
    }

    @GetMapping("/score")
    public List<UserEntity> filterScore(
            @RequestParam int score
    ){
        return userService.filterScore(score);
    }

    @PutMapping("/addall")
    public void addAll(
            @RequestBody List<UserEntity> userList){
        userService.addall(userList);
    }
}
  • This UserApiController is a controller class which is responsible for handling HTTP requests related to UserEntity objects and interacting with the UserService to execute business logic.
  • Within each handler methods, it invokes corresponding methods in UserService which is responsible for interacting with UserRepository.

0개의 댓글