Dependency injection is a design pattern used to achieve loose coupling between classes by providing them with their dependencies instead of hardcoding them.
@Service
public class UserService {
private final UserRepository userRepository;
//Constructor Injection
public UserService(UserRepository userRepository){
this.userRepository = userRepository;
}
}
final, ensuring they can't be reassigned after constructionNullPointerException.Use when building production-grade applications where maintainability and testability are important. Recommended for all new projects.
@RequiredArgsConstructor is a Lombok annotation that generates a constructor for all final fields. @RequiredArgsConstructor simplifies constructor injection by auto-generating the required constructor. @Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
}

BookService(BookRepository) constructor as shown in the screenshot below:
final.Use when we are comfortable with Lombok and want to reduce boilerplate while adhering to best practices.
@Autowired is a spring annotation that injects dependencies directly into fields.@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public void someMethod(){
userRepository.doSomething();
}
}
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.
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.
Responsibilities:
@RequestMapping, GetMapping. @Valid and handle validation errors.Characteristics:
Responsibilities:
@Transactional.Characteristics:
Responsibilities:
User object)Characteristics
Controller receives a client request, validates it, and delegates the request to the Service.Service processes the request, applying business rules, and interacts with the Repository to fetch or persist data.Repository performs data access operations, querying or updating the database. Repository returns the data to the Service, which processes it and returns a response to the Controller.Controller formats the response and sends it back to the client. /api/users/{id} receives a GET request.userService.findUserById(id).userRepository.findById(id).SELECT * FROM users WHERE id = ?)User entity) to the Service layer.User entity into a JSON response and sends it back to the client.PrimaryKey interface.public abstract class Entity implements PrimaryKey{
@Getter
@Setter
private Long id;
}
PrimaryKey Interface:public interface PrimaryKey {
void setId(Long id);
Long getId();
}
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 interface:public interface Repository<T, ID> {}
T is the type of entity (e.g. User, Order)ID is the type of identifier for the entity (e.g. Long, String)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());
}
}
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
//@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);
}
}
@Autowired injects the UserRepository bean into the UserService. This connects the service layer with the repository layer. This is an alternative to using @RequiredArgsConstructor.@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);
}
}
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. UserService which is responsible for interacting with UserRepository.