Repository: optuna/optuna

Issue:
Raise RuntimeError in get_best_trial for a multi-objective study
### Expected behavior

When `get_best_trial` is invoked for a multi-objective study, it raises `ValueError`. But it should raise `RuntimeError` since it is not an error for the wrong argument.

https://github.com/optuna/optuna/blob/739265bec3f98d474036d8a049e2d04e5222a253/optuna/storages/_rdb/storage.py#L1072-L1075

https://github.com/optuna/optuna/blob/af7f11ccc3f0f8840dc4d867a17ba2c68664a032/optuna/storages/_base.py#L607-L608

### Environment

- Optuna version:
- Python version:
- OS:
- (Optional) Other libraries and their versions:


### Error messages, stack traces, or logs

```shell
>>> study._storage.get_best_trial(study._study_id)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/himkt/work/github.com/himkt/optuna/optuna/storages/_in_memory.py", line 290, in get_best_trial
    raise ValueError(
ValueError: Best trial can be obtained only for single-objective optimization.
```
```


### Steps to reproduce

```python
>>> import optuna
>>> study = optuna.create_study(directions=["maximize", "minimize"])
[I 2022-04-24 11:13:34,190] A new study created in memory with name: no-name-40fce37c-44be-46d8-8934-720e1db30cd9
>>> study.tell(study.ask(), [0, 0])
FrozenTrial(number=0, values=[0.0, 0.0], datetime_start=datetime.datetime(2022, 4, 24, 11, 13, 38, 50431), datetime_complete=datetime.datetime(2022, 4, 24, 11, 13, 38, 50708), params={}, distributions={}, user_attrs={}, system_attrs={'nsga2:generation': 0}, intermediate_values={}, trial_id=0, state=TrialState.COMPLETE, value=None)
>>> study._storage.get_best_trial(study._study_id)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/himkt/work/github.com/himkt/optuna/optuna/storages/_in_memory.py", line 290, in get_best_trial
    raise ValueError(
ValueError: Best trial can be obtained only for single-objective optimization.
```

### Additional context (optional)

_No response_


Discussion:

Resolving patch:
diff --git a/optuna/storages/_in_memory.py b/optuna/storages/_in_memory.py
--- a/optuna/storages/_in_memory.py
+++ b/optuna/storages/_in_memory.py
@@ -287,7 +287,7 @@ def get_best_trial(self, study_id: int) -> FrozenTrial:
             if best_trial_id is None:
                 raise ValueError("No trials are completed yet.")
             elif len(self._studies[study_id].directions) > 1:
-                raise ValueError(
+                raise RuntimeError(
                     "Best trial can be obtained only for single-objective optimization."
                 )
             return self.get_trial(best_trial_id)
diff --git a/optuna/storages/_rdb/storage.py b/optuna/storages/_rdb/storage.py
--- a/optuna/storages/_rdb/storage.py
+++ b/optuna/storages/_rdb/storage.py
@@ -1093,7 +1093,7 @@ def get_best_trial(self, study_id: int) -> FrozenTrial:
         with _create_scoped_session(self.scoped_session) as session:
             _directions = self.get_study_directions(study_id)
             if len(_directions) > 1:
-                raise ValueError(
+                raise RuntimeError(
                     "Best trial can be obtained only for single-objective optimization."
                 )
             direction = _directions[0]
diff --git a/optuna/storages/_redis.py b/optuna/storages/_redis.py
--- a/optuna/storages/_redis.py
+++ b/optuna/storages/_redis.py
@@ -515,7 +515,7 @@ def get_best_trial(self, study_id: int) -> FrozenTrial:
 
             _direction = self.get_study_directions(study_id)
             if len(_direction) > 1:
-                raise ValueError(
+                raise RuntimeError(
                     "Best trial can be obtained only for single-objective optimization."
                 )
             direction = _direction[0]


Test patch:
diff --git a/tests/storages_tests/test_storages.py b/tests/storages_tests/test_storages.py
--- a/tests/storages_tests/test_storages.py
+++ b/tests/storages_tests/test_storages.py
@@ -1175,7 +1175,7 @@ def test_get_best_trial_for_multi_objective_optimization(storage_mode: str) -> N
             template_trial.state = TrialState.COMPLETE
             template_trial.values = [i, i + 1]
             storage.create_new_trial(study_id, template_trial=template_trial)
-        with pytest.raises(ValueError):
+        with pytest.raises(RuntimeError):
             storage.get_best_trial(study_id)